├── .github
├── FUNDING.yml
└── workflows
│ ├── build.yml
│ └── golangci.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── LICENSE
├── Makefile
├── README.md
├── annotations.go
├── annotations_test.go
├── build-map-keys.go
├── build.go
├── build_test.go
├── caller.go
├── caller_test.go
├── examples
├── file_output
│ ├── api-documentation.go
│ ├── main.go
│ ├── shared-resources.go
│ └── users_example
│ │ ├── get-user.go
│ │ ├── post-user.go
│ │ └── service.go
└── stream_output
│ ├── logging.go
│ ├── main.go
│ ├── oas-setting.go
│ ├── readme.md
│ └── shared-resources.go
├── go.mod
├── go.sum
├── internal
└── dist
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── index.html
│ ├── oauth2-redirect.html
│ ├── openapi.yaml.example
│ ├── swagger-ui-bundle.js
│ ├── swagger-ui-bundle.js.map
│ ├── swagger-ui-es-bundle-core.js
│ ├── swagger-ui-es-bundle-core.js.map
│ ├── swagger-ui-es-bundle.js
│ ├── swagger-ui-es-bundle.js.map
│ ├── swagger-ui-standalone-preset.js
│ ├── swagger-ui-standalone-preset.js.map
│ ├── swagger-ui.css
│ ├── swagger-ui.css.map
│ ├── swagger-ui.js
│ └── swagger-ui.js.map
├── models.go
├── models_test.go
├── routing.go
├── routing_test.go
├── server.go
├── server_test.go
├── setters.go
└── setters_test.go
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: kaynetik
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on: [ push ]
3 | env:
4 | GO111MODULE: on
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Setup Go for use with actions
10 | uses: actions/setup-go@v2
11 | with:
12 | go-version: 1.15
13 | - uses: actions/checkout@v2
14 | - uses: actions/cache@v2
15 | with:
16 | path: ~/go/pkg/mod
17 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
18 | restore-keys: |
19 | ${{ runner.os }}-go-
20 | - name: Run tests
21 | run: go test -race -covermode atomic -coverprofile=covprofile ./...
22 | - name: Install goveralls
23 | env:
24 | GO111MODULE: off
25 | run: go get github.com/mattn/goveralls
26 | - name: Send coverage
27 | env:
28 | COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
29 | run: goveralls -coverprofile=covprofile
--------------------------------------------------------------------------------
/.github/workflows/golangci.yml:
--------------------------------------------------------------------------------
1 | name: golangci
2 | on: [ push ]
3 | env:
4 | GO111MODULE: on
5 | jobs:
6 | golangci:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v3
10 | - name: golangci-lint
11 | uses: golangci/golangci-lint-action@v3
12 | with:
13 | version: v1.51.2
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .idea/*
3 | #internal/dist/openapi.yaml
4 | testing_out.yaml
5 | ./testing_out.yaml
6 | internal/dist/openapi.yaml
7 | .vscode
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | linters-settings:
2 | gocritic:
3 | enabled-tags:
4 | - diagnostic
5 | - experimental
6 | - opinionated
7 | - performance
8 | - style
9 | goimports:
10 | local-prefixes: github.com/golangci/golangci-lint
11 | govet:
12 | check-shadowing: true
13 | misspell:
14 | locale: US
15 | nakedret:
16 | max-func-lines: 2
17 | gofumpt:
18 | extra-rules: true
19 |
20 | linters:
21 | disable-all: true
22 | enable:
23 | - bodyclose
24 | - depguard
25 | - dogsled
26 | - dupl
27 | - errcheck
28 | - funlen
29 | - gochecknoglobals
30 | - gochecknoinits
31 | - gocognit
32 | - goconst
33 | - gocritic
34 | - gocyclo
35 | - godot
36 | - gofmt
37 | - goimports
38 | - gomnd
39 | - gomodguard
40 | - goprintffuncname
41 | - gosec
42 | - gosimple
43 | - govet
44 | - ineffassign
45 | - lll
46 | - misspell
47 | - nakedret
48 | - nestif
49 | - prealloc
50 | - staticcheck
51 | - stylecheck
52 | - typecheck
53 | - unconvert
54 | - unparam
55 | - unused
56 | - whitespace
57 | - wsl
58 | - asciicheck
59 | - godox
60 | - nolintlint
61 | - exhaustive
62 | - exportloopref
63 | - gofumpt
64 | - goheader
65 | - noctx
66 | - errorlint
67 | - paralleltest
68 | - tparallel
69 | - wrapcheck
70 | - forbidigo
71 | - makezero
72 | - predeclared
73 | - thelper
74 |
75 | issues:
76 | exclude-rules:
77 | - path: examples/*
78 | linters:
79 | - gomnd
80 | - exhaustivestruct
81 | - gochecknoglobals
82 | - path: _test\.go
83 | linters:
84 | - exhaustivestruct
85 | - funlen
86 | - wrapcheck
87 | - path: server.go
88 | linters:
89 | - wrapcheck
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | project_name: go-oas/docs
2 | env:
3 | - GO111MODULE=on
4 | before:
5 | hooks:
6 | - go mod download
7 | builds:
8 | - env:
9 | - CGO_ENABLED=0
10 | main: ./main.go
11 | goos:
12 | - linux
13 | - darwin
14 | - windows
15 | goarch:
16 | - 386
17 | - amd64
18 | - arm
19 | - arm64
20 | ignore:
21 | - goos: darwin
22 | goarch: 386
23 | changelog:
24 | sort: asc
25 | filters:
26 | exclude:
27 | - '^docs:'
28 | - '^test:'
29 | - Merge pull request
30 | - Merge branch
31 | archives:
32 | - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
33 | replacements:
34 | 386: i386
35 | amd64: x86_64
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Copyright 2021 Aleksandar Nešović (github.com/kaynetik)
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | lint:
2 | gofumpt -w ./..
3 | golangci-lint run --fix
4 |
5 | test:
6 | go test ./...
7 |
8 | update_cache:
9 | curl https://sum.golang.org/lookup/github.com/go-oas/docs@v$(VER)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # docs
2 |
3 | ### Automatically generate RESTful API documentation for GO projects - aligned with [Open API Specification standard](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md).
4 |
5 |
6 |
7 | 
8 | 
9 | [](https://github.com/go-oas/docs/releases)
10 | [](https://goreportcard.com/report/github.com/go-oas/docs)
11 | [](https://coveralls.io/github/go-oas/docs?branch=main)
12 | [](https://codebeat.co/projects/github-com-go-oas-docs-main)
13 | [](https://pkg.go.dev/github.com/go-oas/docs)
14 | [](https://awesome-go.com)
15 |
16 | go-OAS Docs converts structured OAS3 (Swagger3) objects into the Open API Specification & automatically serves it on
17 | chosen route and port. It's extremely flexible and simple, so basically it can be integrated into any framework or
18 | existing project.
19 |
20 | We invite anyone interested to join our **[GH Discussions board](https://github.com/go-oas/docs/discussions)**. Honest
21 | feedback will enable us to build better product and at the same time not waste valuable time and effort on something
22 | that might not fit intended usage. So if you can, please spare few minutes to give your opinion of what should be done
23 | next, or what should be the priority for our roadmap. :muscle: :tada:
24 |
25 | ----
26 |
27 | ## Table of Contents
28 |
29 | - [Getting Started](#getting-started)
30 | - [How to use](#how-to-use)
31 | * [Examples](#examples)
32 | - [Contact](#contact)
33 | - [The current roadmap (planned features)](#roadmap)
34 |
35 | ## Getting Started
36 |
37 | 1. Download **_docs_** by using:
38 | ```sh
39 | $ go get -u github.com/go-oas/docs
40 | ```
41 | 2. Add one line annotation to the handler you wish to use in the following
42 | format: `// @OAS `
43 | Examples:
44 | ```
45 | // @OAS handleCreateUser /users POST
46 | // @OAS handleGetUser /users GET
47 | ```
48 | 3. Declare all required documentation elements that are shared. Or reuse ones that already exist in the examples
49 | directory.
50 | 4. Declare specific docs elements per route.
51 |
52 | ----
53 |
54 | ## How to use
55 |
56 | For more explicit example, please refer to [docs/examples](https://github.com/go-oas/docs/examples)
57 |
58 | Add OAS TAG to your existing handler that handles fetching of a User:
59 |
60 | ```go
61 | package users
62 |
63 | import "net/http"
64 |
65 | // @OAS handleGetUser /users GET
66 | func (s *service) handleGetUser() http.HandlerFunc {
67 | return func(w http.ResponseWriter, r *http.Request) {
68 | }
69 | }
70 | ```
71 |
72 | Create a unique API documentation function for that endpoint:
73 |
74 | ```go
75 | package main
76 |
77 | import "github.com/go-oas/docs"
78 |
79 | func handleGetUserRoute(oasPathIndex int, oas *docs.OAS) {
80 | path := oas.GetPathByIndex(oasPathIndex)
81 |
82 | path.Summary = "Get a User"
83 | path.OperationID = "getUser"
84 | path.RequestBody = docs.RequestBody{}
85 | path.Responses = docs.Responses{
86 | getResponseOK(),
87 | }
88 |
89 | path.Tags = append(path.Tags, "pet")
90 | }
91 | ```
92 |
93 | Bear in mind that creating a unique function per endpoint handler is not required, but simply provides good value in
94 | usability of shared documentation elements.
95 |
96 | Once you created the function, simply register it for parsing by using `AttachRoutes()` defined upon `OAS` structure.
97 | E.g.:
98 |
99 | ```go
100 |
101 | package main
102 |
103 | import (
104 | "github.com/go-oas/docs"
105 | )
106 |
107 | func main() {
108 | apiDoc := docs.New()
109 | apiDoc.AttachRoutes([]interface{}{
110 | handleGetUserRoute,
111 | })
112 | ```
113 |
114 | If this approach is too flexible for you, you are always left with the possibility to create your own attacher - or any
115 | other parts of the system for that matter.
116 |
117 | ### Examples
118 |
119 | To run examples, and checkout hosted documentation via Swagger UI, issue the following command:
120 |
121 | ```sh
122 | $ go run ./examples/file_output/*.go
123 | # or uncomment line 40 and comment line 38 in internal/dist/index.html before running:
124 | $ go run ./examples/stream_output/*.go
125 | ```
126 |
127 | And navigate to `http://localhost:3005/docs/api/` in case that you didn't change anything before running the example
128 | above.
129 |
130 | ----
131 |
132 | ## Contact
133 |
134 | Check out the current [Project board](https://github.com/go-oas/docs/projects/1) or
135 | our **[GH Discussions board](https://github.com/go-oas/docs/discussions)** for more information.
136 |
137 | You can join our Telegram group at: [https://t.me/go_oas](https://t.me/go_oas)
138 |
139 | ## Roadmap
140 |
141 | | Feature (GH issues) | Description | Release |
142 | | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------- |
143 | | [Validation](https://github.com/go-oas/docs/issues/17) | Add validation to all structures based on OAS3.0.3 | v1.1.0 |
144 | | [CLI](https://github.com/go-oas/docs/issues/18) | Add CLI support - make it CLI friendly | v1.2.0 |
145 | | [Postman](https://github.com/go-oas/docs/issues/19) | Add postman support via PM API | v1.3.0 |
146 | | [ReDoc](https://github.com/go-oas/docs/issues/20) | Add ReDoc support as an alternative to SwaggerUI | v1.4.0 |
147 | | [E2E Auto-generation](https://github.com/go-oas/docs/issues/21) | Go tests conversion to Cypress/Katalon suites (convert mocked unit tests into e2e tests) | v1.5.0 |
148 |
149 |
--------------------------------------------------------------------------------
/annotations.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "fmt"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 | )
11 |
12 | const (
13 | goFileExt = ".go"
14 | )
15 |
16 | type configAnnotation struct {
17 | getWD getWorkingDirFn
18 | }
19 |
20 | type (
21 | getWorkingDirFn func() (dir string, err error)
22 | pathWalkerFn func(path string, walker walkerFn) (files []string, err error)
23 | walkerFn func(root string, walkFn filepath.WalkFunc) error
24 | )
25 |
26 | // MapAnnotationsInPath scanIn is relevant from initiator calling it.
27 | //
28 | // It accepts the path in which to scan for annotations within Go files.
29 | func (oas *OAS) MapAnnotationsInPath(scanIn string, conf ...configAnnotation) error {
30 | filesInPath, err := scanForChangesInPath(scanIn, getWDFn(conf), walkFilepath)
31 | if err != nil {
32 | return fmt.Errorf(" :%w", err)
33 | }
34 |
35 | for _, file := range filesInPath {
36 | err = oas.mapDocAnnotations(file)
37 | if err != nil {
38 | return fmt.Errorf(" :%w", err)
39 | }
40 | }
41 |
42 | return nil
43 | }
44 |
45 | // scanForChangesInPath scans for annotations changes on handlers in passed path,
46 | // which is relative to the caller's point of view.
47 | func scanForChangesInPath(handlersPath string, getWD getWorkingDirFn, walker pathWalkerFn) (files []string, err error) {
48 | currentPath, err := getWD()
49 | if err != nil {
50 | return files, fmt.Errorf("failed getting current working directory: %w", err)
51 | }
52 |
53 | files, err = walker(filepath.Join(currentPath, handlersPath), filepath.Walk)
54 | if err != nil {
55 | return files, fmt.Errorf("failed walking tree of the given path: %w", err)
56 | }
57 |
58 | return files, nil
59 | }
60 |
61 | func (ca configAnnotation) getCurrentDirFetcher() getWorkingDirFn {
62 | if ca.getWD != nil {
63 | return ca.getWD
64 | }
65 |
66 | return os.Getwd
67 | }
68 |
69 | func getWDFn(configs []configAnnotation) getWorkingDirFn {
70 | if len(configs) != 0 {
71 | return configs[0].getCurrentDirFetcher()
72 | }
73 |
74 | return os.Getwd
75 | }
76 |
77 | func walkFilepath(pathToTraverse string, walker walkerFn) ([]string, error) {
78 | var files []string
79 |
80 | walkFn := func(path string, info os.FileInfo, err error) error {
81 | if info == nil {
82 | return nil
83 | }
84 |
85 | if info.IsDir() {
86 | return nil
87 | }
88 |
89 | if filepath.Ext(path) != goFileExt {
90 | return nil
91 | }
92 |
93 | files = append(files, path)
94 |
95 | return nil
96 | }
97 |
98 | err := walker(pathToTraverse, walkFn)
99 | if err != nil {
100 | return files, err
101 | }
102 |
103 | return files, nil
104 | }
105 |
106 | func (oas *OAS) mapDocAnnotations(path string) error {
107 | if oas == nil {
108 | return errors.New("pointer to OASHandlers can not be nil")
109 | }
110 |
111 | f, err := os.Open(path)
112 | if err != nil {
113 | return fmt.Errorf("failed to open file in path %s :%w", path, err)
114 | }
115 | defer f.Close()
116 |
117 | scanner := bufio.NewScanner(f)
118 |
119 | line := 1
120 |
121 | for scanner.Scan() {
122 | mapIfLineContainsOASTag(scanner.Text(), oas)
123 | line++
124 | }
125 |
126 | err = scanner.Err()
127 | if err != nil {
128 | return fmt.Errorf("scanner failure :%w", err)
129 | }
130 |
131 | return nil
132 | }
133 |
134 | func mapIfLineContainsOASTag(lineText string, o *OAS) {
135 | if strings.Contains(lineText, oasAnnotationInit) {
136 | annotations := oasAnnotations(strings.Fields(lineText))
137 |
138 | var newRoute Path
139 | newRoute.HandlerFuncName = annotations.getHandlerFuncName()
140 | newRoute.Route = annotations.getRoute()
141 | newRoute.HTTPMethod = annotations.getHTTPMethod()
142 |
143 | o.Paths = append(o.Paths, newRoute)
144 | }
145 | }
146 |
147 | type oasAnnotations []string
148 |
149 | func (oa oasAnnotations) getHandlerFuncName() string {
150 | return oa[2]
151 | }
152 |
153 | func (oa oasAnnotations) getRoute() string {
154 | return oa[3]
155 | }
156 |
157 | func (oa oasAnnotations) getHTTPMethod() string {
158 | return oa[4]
159 | }
160 |
--------------------------------------------------------------------------------
/annotations_test.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | import (
4 | "errors"
5 | "math/rand"
6 | "os"
7 | "path/filepath"
8 | "reflect"
9 | "runtime"
10 | "testing"
11 | "testing/quick"
12 | )
13 |
14 | const (
15 | examplesDir = "./examples"
16 | )
17 |
18 | func TestUnitMapAnnotationsInPath(t *testing.T) {
19 | t.Parallel()
20 |
21 | o := prepForInitCallStack(t)
22 |
23 | _ = o.MapAnnotationsInPath(examplesDir)
24 | }
25 |
26 | func TestUnitMapAnnotationsInPathErr(t *testing.T) {
27 | t.Parallel()
28 |
29 | o := (*OAS)(nil)
30 |
31 | err := o.MapAnnotationsInPath(examplesDir)
32 | if err == nil {
33 | t.Error("expected an error, got none")
34 | }
35 | }
36 |
37 | func TestUnitScanForChangesInPathErrFnWD(t *testing.T) {
38 | t.Parallel()
39 |
40 | wd := func() (dir string, err error) {
41 | return "", errors.New("test")
42 | }
43 |
44 | _, err := scanForChangesInPath("", wd, walkFilepath)
45 | if err == nil {
46 | t.Error("expected an error, got none")
47 | }
48 | }
49 |
50 | func TestUnitScanForChangesInPathErrWalk(t *testing.T) {
51 | t.Parallel()
52 |
53 | wd := func() (dir string, err error) {
54 | return "~@!", nil
55 | }
56 | wdErr := func() (dir string, err error) {
57 | return "~@!", errors.New("walkDir error")
58 | }
59 |
60 | pathWalkerErr := func(path string, walker walkerFn) ([]string, error) {
61 | return []string{}, errors.New("triggered")
62 | }
63 |
64 | _, err := scanForChangesInPath("", wd, pathWalkerErr)
65 | if err == nil {
66 | t.Error("expected an error, got none")
67 | }
68 |
69 | o := prepForInitCallStack(t)
70 | errConfig := configAnnotation{
71 | getWD: wdErr,
72 | }
73 |
74 | err = o.MapAnnotationsInPath(".", errConfig)
75 | if err == nil {
76 | t.Error("expected an error, got none")
77 | }
78 |
79 | walkFnErr := func(root string, walkFn filepath.WalkFunc) error {
80 | return errors.New("triggered")
81 | }
82 |
83 | _, err = walkFilepath("", walkFnErr)
84 | if err == nil {
85 | t.Error("expected an error, got none")
86 | }
87 | }
88 |
89 | type triggerNil struct {
90 | trigger bool
91 | }
92 |
93 | func TestQuickUnitGetWD(t *testing.T) {
94 | t.Parallel()
95 |
96 | config := quick.Config{
97 | Values: func(values []reflect.Value, rand *rand.Rand) {
98 | ca := configAnnotation{}
99 | rndNm := rand.Int()
100 | tn := triggerNil{trigger: false}
101 | if rndNm%2 == 0 {
102 | tn.trigger = true
103 | ca.getWD = func() (dir string, err error) {
104 | return "", nil
105 | }
106 | } else {
107 | ca.getWD = os.Getwd
108 | }
109 |
110 | values[0] = reflect.ValueOf(ca)
111 | values[1] = reflect.ValueOf(tn)
112 | },
113 | }
114 |
115 | gwdFetcher := func(ca configAnnotation, tn triggerNil) bool {
116 | got := ca.getCurrentDirFetcher()
117 |
118 | return reflect.TypeOf(got) == reflect.TypeOf(ca.getWD)
119 | }
120 |
121 | if err := quick.Check(gwdFetcher, &config); err != nil {
122 | t.Errorf("Check failed: %#v", err)
123 | }
124 | }
125 |
126 | func TestUnitGWD(t *testing.T) {
127 | t.Parallel()
128 |
129 | ca := configAnnotation{
130 | getWD: func() (dir string, err error) {
131 | return "", nil
132 | },
133 | }
134 |
135 | got := ca.getCurrentDirFetcher()
136 | if reflect.TypeOf(got) != reflect.TypeOf(ca.getWD) {
137 | t.Error("functions differ")
138 | }
139 |
140 | caNil := configAnnotation{}.getCurrentDirFetcher()
141 |
142 | if gFnName(t, caNil) != gFnName(t, os.Getwd) {
143 | t.Error("functions differ")
144 | }
145 | }
146 |
147 | func gFnName(t *testing.T, i interface{}) string {
148 | t.Helper()
149 |
150 | return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
151 | }
152 |
--------------------------------------------------------------------------------
/build-map-keys.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | const (
4 | keyTags = "tags"
5 | keySummary = "summary"
6 | keyOperationID = "operationId"
7 | keySecurity = "security"
8 | keyRequestBody = "requestBody"
9 | keyResponses = "responses"
10 | keyDescription = "description"
11 | keyContent = "content"
12 | keyRef = "$ref"
13 | keySchemas = "schemas"
14 | keySecuritySchemes = "securitySchemes"
15 | keyName = "name"
16 | keyType = "type"
17 | keyProperties = "properties"
18 | keyIn = "in"
19 | keyXML = "xml"
20 | keyFormat = "format"
21 | keyDefault = "default"
22 | keyEnum = "enum"
23 | keyFlows = "flows"
24 | keyAuthorizationURL = "authorizationUrl"
25 | keyScopes = "scopes"
26 | keyParameters = "parameters"
27 | keyRequired = "required"
28 | keySchema = "schema"
29 | )
30 |
--------------------------------------------------------------------------------
/build.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "io"
7 | "os"
8 | "strings"
9 |
10 | "gopkg.in/yaml.v3"
11 | )
12 |
13 | const defaultDocsOutPath = "./internal/dist/openapi.yaml"
14 |
15 | // ConfigBuilder represents a config structure which will be used for the YAML Builder (BuildDocs fn).
16 | //
17 | // This structure was introduced to enable possible extensions to the OAS.BuildDocs()
18 | // without introducing breaking API changes.
19 | type ConfigBuilder struct {
20 | CustomPath string
21 | }
22 |
23 | func (cb ConfigBuilder) getPath() string {
24 | return cb.CustomPath
25 | }
26 |
27 | func getPathFromFirstElement(cbs []ConfigBuilder) string {
28 | if len(cbs) == 0 {
29 | return defaultDocsOutPath
30 | }
31 |
32 | return cbs[0].getPath()
33 | }
34 |
35 | // BuildDocs marshals the OAS struct to YAML and saves it to the chosen output file.
36 | //
37 | // Returns an error if there is any.
38 | func (oas *OAS) BuildDocs(conf ...ConfigBuilder) error {
39 | oas.initCallStackForRoutes()
40 |
41 | yml, err := oas.marshalToYAML()
42 | if err != nil {
43 | return fmt.Errorf("marshaling issue occurred: %w", err)
44 | }
45 |
46 | err = createYAMLOutFile(getPathFromFirstElement(conf), yml)
47 | if err != nil {
48 | return fmt.Errorf("an issue occurred while saving to YAML output: %w", err)
49 | }
50 |
51 | return nil
52 | }
53 |
54 | // BuildStream marshals the OAS struct to YAML and writes it to a stream.
55 | //
56 | // Returns an error if there is any.
57 | func (oas *OAS) BuildStream(w io.Writer) error {
58 | yml, err := oas.marshalToYAML()
59 | if err != nil {
60 | return fmt.Errorf("marshaling issue occurred: %w", err)
61 | }
62 |
63 | err = writeAndFlush(yml, w)
64 | if err != nil {
65 | return fmt.Errorf("writing issue occurred: %w", err)
66 | }
67 |
68 | return nil
69 | }
70 |
71 | func (oas *OAS) marshalToYAML() ([]byte, error) {
72 | transformedOAS := oas.transformToHybridOAS()
73 |
74 | yml, err := yaml.Marshal(transformedOAS)
75 | if err != nil {
76 | return yml, fmt.Errorf("failed marshaling to yaml: %w", err)
77 | }
78 |
79 | return yml, nil
80 | }
81 |
82 | func createYAMLOutFile(outPath string, marshaledYAML []byte) error {
83 | outYAML, err := os.Create(outPath)
84 | if err != nil {
85 | return fmt.Errorf("failed creating yaml output file: %w", err)
86 | }
87 | defer outYAML.Close()
88 |
89 | err = writeAndFlush(marshaledYAML, outYAML)
90 | if err != nil {
91 | return fmt.Errorf("writing issue occurred: %w", err)
92 | }
93 |
94 | return nil
95 | }
96 |
97 | func writeAndFlush(yml []byte, outYAML io.Writer) error {
98 | writer := bufio.NewWriter(outYAML)
99 |
100 | _, err := writer.Write(yml)
101 | if err != nil {
102 | return fmt.Errorf("failed writing to YAML output file: %w", err)
103 | }
104 |
105 | err = writer.Flush()
106 | if err != nil {
107 | return fmt.Errorf("failed flushing output writer: %w", err)
108 | }
109 |
110 | return nil
111 | }
112 |
113 | type (
114 | pathsMap map[string]methodsMap
115 | componentsMap map[string]interface{}
116 | methodsMap map[string]interface{}
117 | pathSecurityMap map[string][]string
118 | pathSecurityMaps []pathSecurityMap
119 | )
120 |
121 | type hybridOAS struct {
122 | OpenAPI OASVersion `yaml:"openapi"`
123 | Info Info `yaml:"info"`
124 | ExternalDocs ExternalDocs `yaml:"externalDocs"`
125 | Servers Servers `yaml:"servers"`
126 | Tags Tags `yaml:"tags"`
127 | Paths pathsMap `yaml:"paths"`
128 | Components componentsMap `yaml:"components"`
129 | }
130 |
131 | func (oas *OAS) transformToHybridOAS() hybridOAS {
132 | ho := hybridOAS{}
133 |
134 | ho.OpenAPI = oas.OASVersion
135 | ho.Info = oas.Info
136 | ho.ExternalDocs = oas.ExternalDocs
137 | ho.Servers = oas.Servers
138 | ho.Tags = oas.Tags
139 |
140 | ho.Paths = makeAllPathsMap(&oas.Paths)
141 | ho.Components = makeComponentsMap(&oas.Components)
142 |
143 | return ho
144 | }
145 |
146 | func makeAllPathsMap(paths *Paths) pathsMap {
147 | allPaths := make(pathsMap, len(*paths))
148 | for _, path := range *paths { //nolint:gocritic //consider indexing?
149 | if allPaths[path.Route] == nil {
150 | allPaths[path.Route] = make(methodsMap)
151 | }
152 |
153 | pathMap := make(map[string]interface{})
154 | pathMap[keyTags] = path.Tags
155 | pathMap[keySummary] = path.Summary
156 | pathMap[keyOperationID] = path.OperationID
157 | pathMap[keyDescription] = path.Description
158 | pathMap[keySecurity] = makeSecurityMap(&path.Security)
159 | pathMap[keyRequestBody] = makeRequestBodyMap(&path.RequestBody)
160 | pathMap[keyResponses] = makeResponsesMap(&path.Responses)
161 | pathMap[keyParameters] = makeParametersMap(path.Parameters)
162 |
163 | allPaths[path.Route][strings.ToLower(path.HTTPMethod)] = pathMap
164 | }
165 |
166 | return allPaths
167 | }
168 |
169 | func makeRequestBodyMap(reqBody *RequestBody) map[string]interface{} {
170 | reqBodyMap := make(map[string]interface{})
171 |
172 | reqBodyMap[keyDescription] = reqBody.Description
173 | reqBodyMap[keyContent] = makeContentSchemaMap(reqBody.Content)
174 |
175 | return reqBodyMap
176 | }
177 |
178 | func makeResponsesMap(responses *Responses) map[string]interface{} {
179 | responsesMap := make(map[string]interface{}, len(*responses))
180 |
181 | for _, resp := range *responses {
182 | codeBodyMap := make(map[string]interface{})
183 | codeBodyMap[keyDescription] = resp.Description
184 | codeBodyMap[keyContent] = makeContentSchemaMap(resp.Content)
185 |
186 | responsesMap[fmt.Sprintf("%d", resp.Code)] = codeBodyMap
187 | }
188 |
189 | return responsesMap
190 | }
191 |
192 | func makeSecurityMap(se *SecurityEntities) pathSecurityMaps {
193 | securityMaps := make(pathSecurityMaps, 0, len(*se))
194 |
195 | for _, sec := range *se {
196 | securityMap := make(pathSecurityMap)
197 | securityMap[sec.AuthName] = sec.PermTypes
198 |
199 | securityMaps = append(securityMaps, securityMap)
200 | }
201 |
202 | return securityMaps
203 | }
204 |
205 | func makeContentSchemaMap(content ContentTypes) map[string]interface{} {
206 | contentSchemaMap := make(map[string]interface{})
207 |
208 | for _, ct := range content {
209 | refMap := make(map[string]string)
210 | refMap[keyRef] = ct.Schema
211 |
212 | schemaMap := make(map[string]map[string]string)
213 | schemaMap["schema"] = refMap
214 |
215 | contentSchemaMap[ct.Name] = schemaMap
216 | }
217 |
218 | return contentSchemaMap
219 | }
220 |
221 | func makeComponentsMap(components *Components) componentsMap {
222 | cm := make(componentsMap, len(*components))
223 |
224 | for _, component := range *components {
225 | cm[keySchemas] = makeComponentSchemasMap(&component.Schemas)
226 | cm[keySecuritySchemes] = makeComponentSecuritySchemesMap(&component.SecuritySchemes)
227 | }
228 |
229 | return cm
230 | }
231 |
232 | func makePropertiesMap(properties *SchemaProperties) map[string]interface{} {
233 | propertiesMap := make(map[string]interface{}, len(*properties))
234 |
235 | for _, prop := range *properties {
236 | propMap := make(map[string]interface{})
237 |
238 | if !isStrEmpty(prop.Type) {
239 | propMap[keyType] = prop.Type
240 | }
241 |
242 | if !isStrEmpty(prop.Format) {
243 | propMap[keyFormat] = prop.Format
244 | }
245 |
246 | if !isStrEmpty(prop.Description) {
247 | propMap[keyDescription] = prop.Description
248 | }
249 |
250 | if len(prop.Enum) > 0 {
251 | propMap[keyEnum] = prop.Enum
252 | }
253 |
254 | if prop.Default != nil {
255 | propMap[keyDefault] = prop.Default
256 | }
257 |
258 | propertiesMap[prop.Name] = propMap
259 | }
260 |
261 | return propertiesMap
262 | }
263 |
264 | func makeComponentSchemasMap(schemas *Schemas) map[string]interface{} {
265 | schemesMap := make(map[string]interface{}, len(*schemas))
266 |
267 | for _, s := range *schemas {
268 | scheme := make(map[string]interface{})
269 |
270 | if s.Ref != "" {
271 | scheme[keyRef] = s.Ref
272 | } else {
273 | scheme[keyType] = s.Type
274 | schemesMap[s.Name] = scheme
275 | scheme[keyProperties] = makePropertiesMap(&s.Properties)
276 |
277 | if s.XML.Name != "" {
278 | scheme[keyXML] = s.XML
279 | }
280 | }
281 | }
282 |
283 | return schemesMap
284 | }
285 |
286 | func makeComponentSecuritySchemesMap(secSchemes *SecuritySchemes) map[string]interface{} {
287 | secSchemesMap := make(map[string]interface{}, len(*secSchemes))
288 |
289 | for _, ss := range *secSchemes {
290 | scheme := make(map[string]interface{})
291 |
292 | lenFlows := len(ss.Flows)
293 |
294 | if !isStrEmpty(ss.Name) && lenFlows == 0 {
295 | scheme[keyName] = ss.Name
296 | }
297 |
298 | if !isStrEmpty(ss.Type) {
299 | scheme[keyType] = ss.Type
300 | }
301 |
302 | if !isStrEmpty(ss.In) {
303 | scheme[keyIn] = ss.In
304 | }
305 |
306 | if lenFlows > 0 {
307 | scheme[keyFlows] = makeFlowsMap(&ss.Flows)
308 | }
309 |
310 | secSchemesMap[ss.Name] = scheme
311 | }
312 |
313 | return secSchemesMap
314 | }
315 |
316 | func makeFlowsMap(flows *SecurityFlows) map[string]interface{} {
317 | flowsMap := make(map[string]interface{}, len(*flows))
318 |
319 | for _, flow := range *flows {
320 | flowMap := make(map[string]interface{})
321 |
322 | flowMap[keyAuthorizationURL] = flow.AuthURL
323 | flowMap[keyScopes] = makeSecurityScopesMap(&flow.Scopes)
324 |
325 | flowsMap[flow.Type] = flowMap
326 | }
327 |
328 | return flowsMap
329 | }
330 |
331 | func makeSecurityScopesMap(scopes *SecurityScopes) map[string]interface{} {
332 | scopesMap := make(map[string]interface{}, len(*scopes))
333 |
334 | for _, scope := range *scopes {
335 | if isStrEmpty(scope.Name) {
336 | continue
337 | }
338 |
339 | scopesMap[scope.Name] = scope.Description
340 | }
341 |
342 | return scopesMap
343 | }
344 |
345 | const emptyStr = ""
346 |
347 | func isStrEmpty(s string) bool {
348 | return s == emptyStr
349 | }
350 |
351 | func makeParametersMap(parameters Parameters) []map[string]interface{} {
352 | parametersMap := []map[string]interface{}{}
353 |
354 | for i := 0; i < len(parameters); i++ {
355 | var (
356 | param = ¶meters[i]
357 | paramMap = make(map[string]interface{})
358 | )
359 |
360 | paramMap[keyName] = param.Name
361 | paramMap[keyIn] = param.In
362 | paramMap[keyDescription] = param.Description
363 | paramMap[keyRequired] = param.Required
364 | paramMap[keySchema] = makeSchemaMap(¶m.Schema)
365 |
366 | parametersMap = append(parametersMap, paramMap)
367 | }
368 |
369 | return parametersMap
370 | }
371 |
372 | func makeSchemaMap(schema *Schema) map[string]interface{} {
373 | schemaMap := make(map[string]interface{})
374 |
375 | if !isStrEmpty(schema.Ref) {
376 | schemaMap[keyRef] = schema.Ref
377 | } else {
378 | schemaMap[keyName] = schema.Name
379 | schemaMap[keyType] = schema.Type
380 | if len(schema.Properties) > 0 {
381 | schemaMap[keyProperties] = makePropertiesMap(&schema.Properties)
382 | }
383 | if schema.XML.Name != "" {
384 | schemaMap[keyXML] = map[string]interface{}{"name": schema.XML.Name}
385 | }
386 | }
387 |
388 | return schemaMap
389 | }
390 |
--------------------------------------------------------------------------------
/build_test.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | import (
4 | "bytes"
5 | "reflect"
6 | "testing"
7 | )
8 |
9 | const (
10 | buildStreamTestWant = `openapi: 3.0.1
11 | info:
12 | title: Test
13 | description: Test object
14 | termsOfService: ""
15 | contact:
16 | email: ""
17 | license:
18 | name: ""
19 | url: ""
20 | version: ""
21 | externalDocs:
22 | description: ""
23 | url: ""
24 | servers: []
25 | tags: []
26 | paths: {}
27 | components:
28 | schemas:
29 | schema_testing:
30 | properties:
31 | EnumProp:
32 | description: short desc
33 | enum:
34 | - enum
35 | - test
36 | - strSlc
37 | type: enum
38 | intProp:
39 | default: 1337
40 | description: short desc
41 | format: int64
42 | type: integer
43 | type: ""
44 | xml:
45 | name: XML entry test
46 | securitySchemes:
47 | ses_scheme_testing:
48 | flows:
49 | implicit:
50 | authorizationUrl: http://petstore.swagger.io/oauth/dialog
51 | scopes:
52 | read:pets: Read Pets
53 | write:pets: Write to Pets
54 | in: not empty
55 | `
56 | )
57 |
58 | func TestUnitBuild(t *testing.T) {
59 | t.Parallel()
60 |
61 | oasPrep := prepForInitCallStack(t)
62 |
63 | setInfoForTest(t, &oasPrep.Info)
64 | setPathForTest(t, &oasPrep.Paths[0])
65 |
66 | components := Components{}
67 | component := Component{
68 | Schemas: Schemas{Schema{
69 | Name: "schema_testing",
70 | Properties: SchemaProperties{
71 | SchemaProperty{
72 | Name: "EnumProp",
73 | Type: "enum",
74 | Description: "short desc",
75 | Enum: []string{"enum", "test", "strSlc"},
76 | },
77 | SchemaProperty{
78 | Name: "intProp",
79 | Type: "integer",
80 | Format: "int64",
81 | Description: "short desc",
82 | Default: 1337,
83 | },
84 | },
85 | XML: XMLEntry{Name: "XML entry test"},
86 | }},
87 | SecuritySchemes: SecuritySchemes{SecurityScheme{
88 | Name: "ses_scheme_testing",
89 | In: "not empty",
90 | Flows: SecurityFlows{SecurityFlow{
91 | Type: "implicit",
92 | AuthURL: "http://petstore.swagger.io/oauth/dialog",
93 | Scopes: SecurityScopes{
94 | SecurityScope{
95 | Name: "write:pets",
96 | Description: "Write to Pets",
97 | },
98 | SecurityScope{
99 | Name: "read:pets",
100 | Description: "Read Pets",
101 | },
102 | },
103 | }},
104 | }},
105 | }
106 | components = append(components, component)
107 | oasPrep.Components = components
108 |
109 | err := oasPrep.BuildDocs(ConfigBuilder{CustomPath: "./testing_out.yaml"})
110 | if err != nil {
111 | t.Errorf("unexpected error for OAS builder: %v", err)
112 | }
113 | }
114 |
115 | func setInfoForTest(t *testing.T, info *Info) {
116 | t.Helper()
117 |
118 | info.Title = "Info Testing"
119 | info.Description = "Not Mandatory"
120 | info.SetContact("aleksandar.nesovic@protonmail.com")
121 | info.SetLicense("MIT", "https://en.wikipedia.org/wiki/MIT_License")
122 | info.Version = "0.0.1"
123 | }
124 |
125 | func setPathForTest(t *testing.T, path *Path) {
126 | t.Helper()
127 |
128 | cts := ContentTypes{ContentType{
129 | Name: "testingType",
130 | Schema: "schema_testing",
131 | }}
132 | response := Response{
133 | Code: 200,
134 | Description: "OK",
135 | Content: cts,
136 | }
137 | responses := Responses{}
138 | responses = append(responses, response)
139 |
140 | path.HTTPMethod = "GET"
141 | path.Tags = []string{}
142 | path.Summary = "TestingSummary"
143 | path.OperationID = "TestingOperationID"
144 | path.RequestBody = RequestBody{
145 | Description: "testReq",
146 | Content: cts,
147 | Required: true,
148 | }
149 | path.Responses = responses
150 | path.Security = SecurityEntities{Security{AuthName: "sec"}}
151 | }
152 |
153 | func TestUnitGetPathFromFirstElem(t *testing.T) {
154 | t.Parallel()
155 |
156 | cbs := make([]ConfigBuilder, 0)
157 | got := getPathFromFirstElement(cbs)
158 |
159 | if got != defaultDocsOutPath {
160 | t.Error("default docs path not set correctly")
161 | }
162 | }
163 |
164 | // QUICK CHECK TESTS ARE COMING WITH NEXT RELEASE.
165 |
166 | func TestOAS_BuildStream(t *testing.T) {
167 | t.Parallel()
168 |
169 | tests := []struct {
170 | name string
171 | oas *OAS
172 | wantW string
173 | wantErr bool
174 | }{
175 | {
176 | name: "success",
177 | oas: &OAS{
178 | OASVersion: "3.0.1",
179 | Info: Info{Title: "Test", Description: "Test object"},
180 | Components: Components{
181 | Component{
182 | Schemas: Schemas{Schema{
183 | Name: "schema_testing",
184 | Properties: SchemaProperties{
185 | SchemaProperty{
186 | Name: "EnumProp", Type: "enum", Description: "short desc",
187 | Enum: []string{"enum", "test", "strSlc"},
188 | },
189 | SchemaProperty{
190 | Name: "intProp", Type: "integer", Format: "int64",
191 | Description: "short desc", Default: 1337,
192 | },
193 | },
194 | XML: XMLEntry{Name: "XML entry test"},
195 | }},
196 | SecuritySchemes: SecuritySchemes{SecurityScheme{
197 | Name: "ses_scheme_testing",
198 | In: "not empty",
199 | Flows: SecurityFlows{SecurityFlow{
200 | Type: "implicit",
201 | AuthURL: "http://petstore.swagger.io/oauth/dialog",
202 | Scopes: SecurityScopes{
203 | SecurityScope{Name: "write:pets", Description: "Write to Pets"},
204 | SecurityScope{Name: "read:pets", Description: "Read Pets"},
205 | },
206 | }},
207 | }},
208 | },
209 | },
210 | },
211 | wantErr: false,
212 | wantW: buildStreamTestWant,
213 | },
214 | }
215 |
216 | for _, tt := range tests {
217 | trn := tt
218 |
219 | t.Run(trn.name, func(t *testing.T) {
220 | t.Parallel()
221 |
222 | w := &bytes.Buffer{}
223 | if err := trn.oas.BuildStream(w); (err != nil) != trn.wantErr {
224 | t.Errorf("OAS.BuildStream() error = %v, wantErr %v", err, trn.wantErr)
225 | return
226 | }
227 | if gotW := w.String(); gotW != trn.wantW {
228 | t.Errorf("OAS.BuildStream() = [%v], want {%v}", gotW, trn.wantW)
229 | }
230 | })
231 | }
232 | }
233 |
234 | func Test_makeParametersMap(t *testing.T) {
235 | t.Parallel()
236 |
237 | type args struct {
238 | parameters Parameters
239 | }
240 |
241 | tests := []struct {
242 | name string
243 | args args
244 | want []map[string]interface{}
245 | }{
246 | {
247 | name: "success-minimal",
248 | args: args{
249 | parameters: Parameters{{
250 | Name: "id",
251 | In: "path",
252 | Description: "test",
253 | Required: true,
254 | Schema: Schema{Name: "id", Type: "integer"},
255 | }},
256 | },
257 | want: []map[string]interface{}{{
258 | "name": "id",
259 | "in": "path",
260 | "description": "test",
261 | "required": true,
262 | "schema": map[string]interface{}{"name": "id", "type": "integer"},
263 | }},
264 | },
265 | {
266 | name: "success-full",
267 | args: args{
268 | parameters: Parameters{{
269 | Name: "id",
270 | In: "path",
271 | Description: "test",
272 | Required: true,
273 | Schema: Schema{
274 | Name: "id",
275 | Type: "integer",
276 | Properties: SchemaProperties{{Name: "id", Type: "integer"}},
277 | },
278 | }},
279 | },
280 | want: []map[string]interface{}{{
281 | "name": "id",
282 | "in": "path",
283 | "description": "test",
284 | "required": true,
285 | "schema": map[string]interface{}{
286 | "name": "id",
287 | "type": "integer",
288 | "properties": map[string]interface{}{
289 | "id": map[string]interface{}{"type": "integer"},
290 | },
291 | },
292 | }},
293 | },
294 | {
295 | name: "success-ref",
296 | args: args{
297 | parameters: Parameters{{
298 | Name: "id",
299 | In: "path",
300 | Description: "test",
301 | Required: true,
302 | Schema: Schema{Ref: "$some-ref"},
303 | }},
304 | },
305 | want: []map[string]interface{}{{
306 | "name": "id",
307 | "in": "path",
308 | "description": "test",
309 | "required": true,
310 | "schema": map[string]interface{}{"$ref": "$some-ref"},
311 | }},
312 | },
313 | {
314 | name: "success-xml-entry",
315 | args: args{
316 | parameters: Parameters{{
317 | Name: "id",
318 | In: "path",
319 | Description: "test",
320 | Required: true,
321 | Schema: Schema{Name: "id", Type: "integer", XML: XMLEntry{Name: "id"}},
322 | }},
323 | },
324 | want: []map[string]interface{}{{
325 | "name": "id",
326 | "in": "path",
327 | "description": "test",
328 | "required": true,
329 | "schema": map[string]interface{}{"name": "id", "type": "integer", "xml": map[string]interface{}{"name": "id"}},
330 | }},
331 | },
332 | }
333 | for _, tt := range tests {
334 | trn := tt
335 |
336 | t.Run(trn.name, func(t *testing.T) {
337 | t.Parallel()
338 |
339 | if got := makeParametersMap(trn.args.parameters); !reflect.DeepEqual(got, trn.want) {
340 | t.Errorf("makeParametersMap() = %+v, want %+v", got, trn.want)
341 | }
342 | })
343 | }
344 | }
345 |
--------------------------------------------------------------------------------
/caller.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | import (
4 | "reflect"
5 | )
6 |
7 | // routePostfix will get exported by v1.3.
8 | const routePostfix = "Route"
9 |
10 | // Call is used init registered functions that already exist in the *OAS, and return results if there are any.
11 | func (oas *OAS) Call(name string, params ...interface{}) (result []reflect.Value) {
12 | f := reflect.ValueOf(oas.RegisteredRoutes[name])
13 |
14 | in := make([]reflect.Value, len(params))
15 | for k, param := range params {
16 | in[k] = reflect.ValueOf(param)
17 | }
18 |
19 | result = f.Call(in)
20 |
21 | return result
22 | }
23 |
24 | func (oas *OAS) initCallStackForRoutes() {
25 | for oasPathIndex := range oas.Paths {
26 | oas.Call(oas.Paths[oasPathIndex].HandlerFuncName+routePostfix, oasPathIndex, oas)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/caller_test.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | import (
4 | "math/rand"
5 | "reflect"
6 | "testing"
7 | "testing/quick"
8 | )
9 |
10 | const testingPostfix = "Testing"
11 |
12 | func TestQuickUnitCaller(t *testing.T) {
13 | t.Parallel()
14 |
15 | successParamNumber := func(i int, oas *OAS) {}
16 | config := quick.Config{
17 | Values: func(args []reflect.Value, rand *rand.Rand) {
18 | rr := make(RegRoutes)
19 | count := rand.Intn(550-1) + 1
20 | paths := getPaths(t, count, RandomString(t, count))
21 |
22 | for i := 0; i < count; i++ {
23 | rr[paths[0].Route+testingPostfix] = successParamNumber
24 | }
25 |
26 | oas := OAS{
27 | Paths: paths,
28 | RegisteredRoutes: rr,
29 | }
30 |
31 | args[0] = reflect.ValueOf(oas)
32 | },
33 | }
34 |
35 | callerCorrectParamNumber := func(oas OAS) bool {
36 | for i, oasPath := range oas.Paths {
37 | res := oas.Call(oasPath.Route+testingPostfix, i, &oas)
38 |
39 | if len(res) > 0 {
40 | t.Error("failed executing (OAS).Call() with")
41 |
42 | return false
43 | }
44 | }
45 |
46 | return true
47 | }
48 |
49 | if err := quick.Check(callerCorrectParamNumber, &config); err != nil {
50 | t.Errorf("Check failed: %#v", err)
51 | }
52 | }
53 |
54 | func TestUnitCaller(t *testing.T) {
55 | t.Parallel()
56 |
57 | successParamNumber := func(i int, oas *OAS) {}
58 | routeName := "testRouteTesting"
59 | rr := make(RegRoutes)
60 | rr[routeName] = successParamNumber
61 |
62 | o := OAS{
63 | RegisteredRoutes: rr,
64 | }
65 |
66 | _ = o.Call(routeName, 0, &o)
67 | }
68 |
69 | func TestUnitInitCallStack(t *testing.T) {
70 | t.Parallel()
71 |
72 | o := prepForInitCallStack(t)
73 |
74 | o.initCallStackForRoutes()
75 | }
76 |
77 | func prepForInitCallStack(t *testing.T) OAS {
78 | t.Helper()
79 |
80 | routeName := "testRoute" + routePostfix
81 | rr := make(RegRoutes)
82 |
83 | rr[routeName] = getSuccessParamNumber
84 |
85 | path := Path{
86 | HandlerFuncName: "testRoute",
87 | }
88 | o := OAS{
89 | Paths: Paths{path},
90 | RegisteredRoutes: rr,
91 | }
92 |
93 | return o
94 | }
95 |
96 | func getSuccessParamNumber(_ int, _ *OAS) {}
97 |
98 | // func getFailureParamNumber(t *testing.T, _ int, _ *OAS) { t.Helper() }
99 |
--------------------------------------------------------------------------------
/examples/file_output/api-documentation.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/go-oas/docs"
4 |
5 | func handleCreateUserRoute(oasPathIndex int, oas *docs.OAS) {
6 | path := oas.GetPathByIndex(oasPathIndex)
7 |
8 | path.Description = "Create a new User"
9 | path.OperationID = "createUser"
10 |
11 | path.RequestBody = docs.RequestBody{
12 | Description: "Create a new User",
13 | Content: docs.ContentTypes{
14 | getContentApplicationJSON("#/components/schemas/User"),
15 | },
16 | Required: true,
17 | }
18 |
19 | path.Responses = docs.Responses{
20 | getResponseNotFound(),
21 | getResponseOK(),
22 | }
23 |
24 | path.Security = docs.SecurityEntities{
25 | docs.Security{
26 | AuthName: "petstore_auth",
27 | PermTypes: []string{"write:users", "read:users"},
28 | },
29 | }
30 |
31 | path.Tags = append(path.Tags, "user")
32 | }
33 |
34 | func handleGetUserRoute(oasPathIndex int, oas *docs.OAS) {
35 | path := oas.GetPathByIndex(oasPathIndex)
36 |
37 | path.Description = "Get a User"
38 | path.OperationID = "getUser"
39 | path.RequestBody = docs.RequestBody{}
40 | path.Responses = docs.Responses{
41 | getResponseOK(),
42 | }
43 |
44 | path.Tags = append(path.Tags, "pet")
45 | }
46 |
--------------------------------------------------------------------------------
/examples/file_output/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/go-oas/docs"
5 | )
6 |
7 | func main() {
8 | apiDoc := docs.New()
9 |
10 | apiSetInfo(&apiDoc)
11 | apiSetTags(&apiDoc)
12 | apiSetServers(&apiDoc)
13 | apiSetExternalDocs(&apiDoc)
14 | apiSetComponents(&apiDoc)
15 |
16 | apiDoc.AttachRoutes([]docs.RouteFn{
17 | handleCreateUserRoute,
18 | handleGetUserRoute,
19 | })
20 |
21 | err := apiDoc.MapAnnotationsInPath("./examples/users_example")
22 | if err != nil {
23 | panic(err)
24 | }
25 |
26 | err = apiDoc.BuildDocs()
27 | if err != nil {
28 | panic(err)
29 | }
30 |
31 | err = docs.ServeSwaggerUI(&docs.ConfigSwaggerUI{
32 | Route: "/docs/api/",
33 | Port: "3005",
34 | })
35 | if err != nil {
36 | panic(err)
37 | }
38 | }
39 |
40 | func apiSetInfo(apiDoc *docs.OAS) {
41 | apiDoc.SetOASVersion("3.0.1")
42 | apiInfo := apiDoc.GetInfo()
43 | apiInfo.Title = "Build OAS3.0.1"
44 | apiInfo.Description = "Description - Builder Testing for OAS3.0.1"
45 | apiInfo.TermsOfService = "https://smartbear.com/terms-of-use/"
46 | apiInfo.SetContact("aleksandar.nesovic@protonmail.com") // mixed usage of setters ->
47 | apiInfo.License = docs.License{ // and direct struct usage.
48 | Name: "MIT",
49 | URL: "https://github.com/go-oas/docs/blob/main/LICENSE",
50 | }
51 | apiInfo.Version = "1.0.1"
52 | }
53 |
54 | func apiSetTags(apiDoc *docs.OAS) {
55 | // With Tags example you can see usage of direct struct modifications, setter and appender as well.
56 | apiDoc.Tags = docs.Tags{
57 | docs.Tag{
58 | Name: "user",
59 | Description: "Operations about the User",
60 | ExternalDocs: docs.ExternalDocs{
61 | Description: "User from the Petstore example",
62 | URL: "http://swagger.io",
63 | },
64 | },
65 | }
66 | apiDoc.Tags.SetTag(
67 | "pet",
68 | "Everything about your Pets",
69 | docs.ExternalDocs{
70 | Description: "Find out more about our store (Swagger UI Example)",
71 | URL: "http://swagger.io",
72 | },
73 | )
74 |
75 | newTag := &docs.Tag{
76 | Name: "petko",
77 | Description: "Everything about your Petko",
78 | ExternalDocs: docs.ExternalDocs{
79 | Description: "Find out more about our store (Swagger UI Example)",
80 | URL: "http://swagger.io",
81 | },
82 | }
83 | apiDoc.Tags.AppendTag(newTag)
84 | }
85 |
86 | func apiSetServers(apiDoc *docs.OAS) {
87 | apiDoc.Servers = docs.Servers{
88 | docs.Server{
89 | URL: "https://petstore.swagger.io/v2",
90 | },
91 | docs.Server{
92 | URL: "http://petstore.swagger.io/v2",
93 | },
94 | }
95 | }
96 |
97 | func apiSetExternalDocs(apiDoc *docs.OAS) {
98 | apiDoc.ExternalDocs = docs.ExternalDocs{
99 | Description: "External documentation",
100 | URL: "https://kaynetik.com",
101 | }
102 | }
103 |
104 | func apiSetComponents(apiDoc *docs.OAS) {
105 | apiDoc.Components = docs.Components{
106 | docs.Component{
107 | Schemas: docs.Schemas{
108 | docs.Schema{
109 | Name: "User",
110 | Type: "object",
111 | Properties: docs.SchemaProperties{
112 | docs.SchemaProperty{
113 | Name: "id",
114 | Type: "integer",
115 | Format: "int64",
116 | Description: "UserID",
117 | },
118 | docs.SchemaProperty{
119 | Name: "username",
120 | Type: "string",
121 | },
122 | docs.SchemaProperty{
123 | Name: "email",
124 | Type: "string",
125 | },
126 | docs.SchemaProperty{
127 | Name: "userStatus",
128 | Type: "integer",
129 | Description: "User Status",
130 | Format: "int32",
131 | },
132 | docs.SchemaProperty{
133 | Name: "phForEnums",
134 | Type: "enum",
135 | Enum: []string{"placed", "approved"},
136 | },
137 | },
138 | XML: docs.XMLEntry{Name: "User"},
139 | },
140 | docs.Schema{
141 | Name: "Tag",
142 | Type: "object",
143 | Properties: docs.SchemaProperties{
144 | docs.SchemaProperty{
145 | Name: "id",
146 | Type: "integer",
147 | Format: "int64",
148 | },
149 | docs.SchemaProperty{
150 | Name: "name",
151 | Type: "string",
152 | },
153 | },
154 | XML: docs.XMLEntry{Name: "Tag"},
155 | },
156 | docs.Schema{
157 | Name: "ApiResponse",
158 | Type: "object",
159 | Properties: docs.SchemaProperties{
160 | docs.SchemaProperty{
161 | Name: "code",
162 | Type: "integer",
163 | Format: "int32",
164 | },
165 | docs.SchemaProperty{
166 | Name: "type",
167 | Type: "string",
168 | },
169 | docs.SchemaProperty{
170 | Name: "message",
171 | Type: "string",
172 | },
173 | },
174 | XML: docs.XMLEntry{Name: "ApiResponse"},
175 | },
176 | },
177 | SecuritySchemes: docs.SecuritySchemes{
178 | docs.SecurityScheme{
179 | Name: "api_key",
180 | Type: "apiKey",
181 | In: "header",
182 | },
183 | docs.SecurityScheme{
184 | Name: "petstore_auth",
185 | Type: "oauth2",
186 | Flows: docs.SecurityFlows{
187 | docs.SecurityFlow{
188 | Type: "implicit",
189 | AuthURL: "http://petstore.swagger.io/oauth/dialog",
190 | Scopes: docs.SecurityScopes{
191 | docs.SecurityScope{
192 | Name: "write:users",
193 | Description: "Modify users",
194 | },
195 | docs.SecurityScope{
196 | Name: "read:users",
197 | Description: "Read users",
198 | },
199 | },
200 | },
201 | },
202 | },
203 | },
204 | },
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/examples/file_output/shared-resources.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/go-oas/docs"
4 |
5 | // Args packer could be used to improve readability of such functions,
6 | // and to make the more flexible in order to avoid empty string comparisons.
7 | func getResponseOK(args ...interface{}) docs.Response {
8 | description := "OK"
9 | if args != nil {
10 | description = args[0].(string)
11 | }
12 |
13 | return docs.Response{
14 | Code: 200,
15 | Description: description,
16 | }
17 | }
18 |
19 | func getResponseNotFound() docs.Response {
20 | return docs.Response{
21 | Code: 404,
22 | Description: "Not Found",
23 | Content: docs.ContentTypes{
24 | getContentApplicationJSON("#/components/schemas/User"),
25 | },
26 | }
27 | }
28 |
29 | func getContentApplicationJSON(refSchema string) docs.ContentType {
30 | return docs.ContentType{
31 | Name: "application/json",
32 | Schema: refSchema,
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/file_output/users_example/get-user.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import "net/http"
4 |
5 | // @OAS handleGetUser /users GET
6 | func (s *service) handleGetUser() http.HandlerFunc {
7 | return func(w http.ResponseWriter, r *http.Request) {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/file_output/users_example/post-user.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | // @OAS handleCreateUser /users POST
8 | func (s *service) handleCreateUser() http.HandlerFunc {
9 | return func(w http.ResponseWriter, r *http.Request) {
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/examples/file_output/users_example/service.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | type service struct {
4 | repo exampleRepo
5 | }
6 |
7 | type exampleRepo interface {
8 | CreateUser() error
9 | }
10 |
11 | func newService(repo exampleRepo) service {
12 | return service{
13 | repo,
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/stream_output/logging.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | type responseData struct {
10 | status int
11 | size int
12 | }
13 |
14 | // our http.ResponseWriter implementation
15 | type loggingResponseWriter struct {
16 | http.ResponseWriter // compose original http.ResponseWriter
17 | responseData *responseData
18 | }
19 |
20 | func (r *loggingResponseWriter) Write(b []byte) (int, error) {
21 | size, err := r.ResponseWriter.Write(b) // write response using original http.ResponseWriter
22 | r.responseData.size += size // capture size
23 | return size, err
24 | }
25 |
26 | func (r *loggingResponseWriter) WriteHeader(statusCode int) {
27 | r.ResponseWriter.WriteHeader(statusCode) // write status code using original http.ResponseWriter
28 | r.responseData.status = statusCode // capture status code
29 | }
30 |
31 | func LogginMiddleware(h http.Handler) http.Handler {
32 | return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
33 | var (
34 | start = time.Now()
35 | responseData = &responseData{}
36 | )
37 |
38 | h.ServeHTTP(&loggingResponseWriter{
39 | ResponseWriter: rw,
40 | responseData: responseData,
41 | }, req)
42 |
43 | duration := time.Since(start)
44 |
45 | log.Printf("%s[%v] uri:%s duration:%v size:%d", req.Method, responseData.status, req.RequestURI, duration, responseData.size)
46 | })
47 | }
48 |
--------------------------------------------------------------------------------
/examples/stream_output/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net/http"
7 | "os"
8 |
9 | "github.com/go-oas/docs"
10 | )
11 |
12 | const (
13 | staticRoute = "/docs/api/"
14 | streamRoute = "/docs/oas/"
15 | staticDirectory = "./internal/dist"
16 | port = 3005
17 | )
18 |
19 | func main() {
20 | apiDoc := docs.New()
21 | apiSetInfo(&apiDoc)
22 | apiSetTags(&apiDoc)
23 | apiSetServers(&apiDoc)
24 | apiSetExternalDocs(&apiDoc)
25 | apiSetComponents(&apiDoc)
26 |
27 | apiDoc.AddRoute(&docs.Path{
28 | Route: "/users",
29 | HTTPMethod: "POST",
30 | OperationID: "createUser",
31 | Summary: "Create a new User",
32 | Responses: docs.Responses{
33 | getResponseOK(),
34 | getResponseNotFound(),
35 | },
36 | // HandlerFuncName: "handleCreateUser",
37 | RequestBody: docs.RequestBody{
38 | Description: "Create a new User",
39 | Content: docs.ContentTypes{
40 | getContentApplicationJSON("#/components/schemas/User"),
41 | },
42 | Required: true,
43 | },
44 | })
45 |
46 | apiDoc.AddRoute(&docs.Path{
47 | Route: "/users",
48 | HTTPMethod: "GET",
49 | OperationID: "getUser",
50 | Summary: "Get Users list",
51 | Responses: docs.Responses{
52 | getResponseOK(),
53 | },
54 | // HandlerFuncName: "handleCreateUser",
55 | RequestBody: docs.RequestBody{
56 | Description: "Get Users list",
57 | Content: docs.ContentTypes{
58 | getContentApplicationJSON("#/components/schemas/User"),
59 | },
60 | Required: true,
61 | },
62 | })
63 |
64 | apiDoc.AddRoute(&docs.Path{
65 | Route: "/users/{id}",
66 | HTTPMethod: "GET",
67 | OperationID: "getUser",
68 | Summary: "Get a User",
69 | Responses: docs.Responses{
70 | getResponseOK(),
71 | },
72 | // HandlerFuncName: "handleCreateUser",
73 | RequestBody: docs.RequestBody{
74 | Description: "Get a user",
75 | Content: docs.ContentTypes{
76 | getContentApplicationJSON("#/components/schemas/User"),
77 | },
78 | Required: true,
79 | },
80 | Parameters: docs.Parameters{{
81 | Name: "id",
82 | Description: "User ID",
83 | In: "path",
84 | Required: true,
85 | Schema: docs.Schema{
86 | Name: "id",
87 | Type: "string",
88 | },
89 | }},
90 | })
91 |
92 | mux := http.NewServeMux()
93 |
94 | // serve static files
95 | fs := http.FileServer(http.Dir(staticDirectory))
96 | mux.Handle(staticRoute, http.StripPrefix(staticRoute, fs))
97 |
98 | // serve the oas document from a stream
99 | mux.HandleFunc(streamRoute, func(w http.ResponseWriter, r *http.Request) {
100 | w.Header().Set("Content-Type", "text/yaml")
101 | if err := apiDoc.BuildStream(w); err != nil {
102 | http.Error(w, "could not write body", http.StatusInternalServerError)
103 | return
104 | }
105 | })
106 |
107 | fmt.Printf("Listening at :%d", port)
108 | if err := http.ListenAndServe(fmt.Sprintf(":%d", port), LogginMiddleware(mux)); err != nil {
109 | if errors.Is(err, http.ErrServerClosed) {
110 | fmt.Printf("server closed\n")
111 | } else if err != nil {
112 | fmt.Printf("error starting server: %s\n", err)
113 | os.Exit(1)
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/examples/stream_output/oas-setting.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/go-oas/docs"
4 |
5 | func apiSetInfo(apiDoc *docs.OAS) {
6 | apiDoc.SetOASVersion("3.0.1")
7 | apiInfo := apiDoc.GetInfo()
8 | apiInfo.Title = "Build OAS3.0.1"
9 | apiInfo.Description = "Builder Testing for OAS3.0.1"
10 | apiInfo.TermsOfService = "https://smartbear.com/terms-of-use/"
11 | apiInfo.SetContact("padiazg@gmail.com") // mixed usage of setters ->
12 | apiInfo.License = docs.License{ // and direct struct usage.
13 | Name: "MIT",
14 | URL: "https://github.com/go-oas/docs/blob/main/LICENSE",
15 | }
16 | apiInfo.Version = "1.0.1"
17 | }
18 |
19 | func apiSetTags(apiDoc *docs.OAS) {
20 | // With Tags example you can see usage of direct struct modifications, setter and appender as well.
21 | apiDoc.Tags = docs.Tags{
22 | docs.Tag{
23 | Name: "user",
24 | Description: "Operations about the User",
25 | ExternalDocs: docs.ExternalDocs{
26 | Description: "User from the Petstore example",
27 | URL: "http://swagger.io",
28 | },
29 | },
30 | }
31 | apiDoc.Tags.SetTag(
32 | "pet",
33 | "Everything about your Pets",
34 | docs.ExternalDocs{
35 | Description: "Find out more about our store (Swagger UI Example)",
36 | URL: "http://swagger.io",
37 | },
38 | )
39 |
40 | newTag := &docs.Tag{
41 | Name: "petko",
42 | Description: "Everything about your Petko",
43 | ExternalDocs: docs.ExternalDocs{
44 | Description: "Find out more about our store (Swagger UI Example)",
45 | URL: "http://swagger.io",
46 | },
47 | }
48 | apiDoc.Tags.AppendTag(newTag)
49 | }
50 |
51 | func apiSetServers(apiDoc *docs.OAS) {
52 | apiDoc.Servers = docs.Servers{
53 | docs.Server{
54 | URL: "https://petstore.swagger.io/v2",
55 | },
56 | docs.Server{
57 | URL: "http://petstore.swagger.io/v2",
58 | },
59 | }
60 | }
61 |
62 | func apiSetExternalDocs(apiDoc *docs.OAS) {
63 | apiDoc.ExternalDocs = docs.ExternalDocs{
64 | Description: "External documentation",
65 | URL: "https://kaynetik.com",
66 | }
67 | }
68 |
69 | func apiSetComponents(apiDoc *docs.OAS) {
70 | apiDoc.Components = docs.Components{
71 | docs.Component{
72 | Schemas: docs.Schemas{
73 | docs.Schema{
74 | Name: "User",
75 | Type: "object",
76 | Properties: docs.SchemaProperties{
77 | docs.SchemaProperty{
78 | Name: "id",
79 | Type: "integer",
80 | Format: "int64",
81 | Description: "UserID",
82 | },
83 | docs.SchemaProperty{
84 | Name: "username",
85 | Type: "string",
86 | },
87 | docs.SchemaProperty{
88 | Name: "email",
89 | Type: "string",
90 | },
91 | docs.SchemaProperty{
92 | Name: "userStatus",
93 | Type: "integer",
94 | Description: "User Status",
95 | Format: "int32",
96 | },
97 | docs.SchemaProperty{
98 | Name: "phForEnums",
99 | Type: "enum",
100 | Enum: []string{"placed", "approved"},
101 | },
102 | },
103 | XML: docs.XMLEntry{Name: "User"},
104 | },
105 | docs.Schema{
106 | Name: "Tag",
107 | Type: "object",
108 | Properties: docs.SchemaProperties{
109 | docs.SchemaProperty{
110 | Name: "id",
111 | Type: "integer",
112 | Format: "int64",
113 | },
114 | docs.SchemaProperty{
115 | Name: "name",
116 | Type: "string",
117 | },
118 | },
119 | XML: docs.XMLEntry{Name: "Tag"},
120 | },
121 | docs.Schema{
122 | Name: "ApiResponse",
123 | Type: "object",
124 | Properties: docs.SchemaProperties{
125 | docs.SchemaProperty{
126 | Name: "code",
127 | Type: "integer",
128 | Format: "int32",
129 | },
130 | docs.SchemaProperty{
131 | Name: "type",
132 | Type: "string",
133 | },
134 | docs.SchemaProperty{
135 | Name: "message",
136 | Type: "string",
137 | },
138 | },
139 | XML: docs.XMLEntry{Name: "ApiResponse"},
140 | },
141 | },
142 | SecuritySchemes: docs.SecuritySchemes{
143 | docs.SecurityScheme{
144 | Name: "api_key",
145 | Type: "apiKey",
146 | In: "header",
147 | },
148 | docs.SecurityScheme{
149 | Name: "petstore_auth",
150 | Type: "oauth2",
151 | Flows: docs.SecurityFlows{
152 | docs.SecurityFlow{
153 | Type: "implicit",
154 | AuthURL: "http://petstore.swagger.io/oauth/dialog",
155 | Scopes: docs.SecurityScopes{
156 | docs.SecurityScope{
157 | Name: "write:users",
158 | Description: "Modify users",
159 | },
160 | docs.SecurityScope{
161 | Name: "read:users",
162 | Description: "Read users",
163 | },
164 | },
165 | },
166 | },
167 | },
168 | },
169 | },
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/examples/stream_output/readme.md:
--------------------------------------------------------------------------------
1 | # Stream output example
2 | This example shows how to use the `BuildStream` function to generate the document into a stream. You can use this stream to send the document to any destination that accepts a stream, like an HTTP response or a file.
3 |
4 | # Annotations
5 | The example also shows that now we can add routes without parsing the code looking for annotations. This feature can be helpful in several use cases, like generating the documentation from a framework, or some definition or manifest because you don't have access to code to write annotations.
6 |
7 | ## Update index.html
8 | This example serves the document in the `/docs/oas`, no file is generated, and the renderer in `/docs/api`. To correctly render the document you must uncomment line 40 and make sure lines 38 and 39 are commented in `internal/dist/index.html`
9 |
10 | ```html
11 | ...
12 | window.ui = SwaggerUIBundle({
13 | // url: "openapi.yaml",
14 | // url: "https://petstore.swagger.io/v2/swagger.json",
15 | url: "/docs/oas",
16 | ...
17 | ```
18 |
--------------------------------------------------------------------------------
/examples/stream_output/shared-resources.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/go-oas/docs"
4 |
5 | // Args packer could be used to improve readability of such functions,
6 | // and to make the more flexible in order to avoid empty string comparisons.
7 | func getResponseOK(args ...interface{}) docs.Response {
8 | description := "OK"
9 | if args != nil {
10 | description = args[0].(string)
11 | }
12 |
13 | return docs.Response{
14 | Code: 200,
15 | Description: description,
16 | }
17 | }
18 |
19 | func getResponseNotFound() docs.Response {
20 | return docs.Response{
21 | Code: 404,
22 | Description: "Not Found",
23 | Content: docs.ContentTypes{
24 | getContentApplicationJSON("#/components/schemas/User"),
25 | },
26 | }
27 | }
28 |
29 | func getContentApplicationJSON(refSchema string) docs.ContentType {
30 | return docs.ContentType{
31 | Name: "application/json",
32 | Schema: refSchema,
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/go-oas/docs
2 |
3 | go 1.19
4 |
5 | require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
6 |
7 | require (
8 | github.com/kr/pretty v0.1.0 // indirect
9 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
10 | )
11 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
2 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
3 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
4 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
5 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
6 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
7 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
8 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
10 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
11 |
--------------------------------------------------------------------------------
/internal/dist/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-oas/docs/6e09fa6b336d503d51191a0bc4ac75a923b6f89c/internal/dist/favicon-16x16.png
--------------------------------------------------------------------------------
/internal/dist/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-oas/docs/6e09fa6b336d503d51191a0bc4ac75a923b6f89c/internal/dist/favicon-32x32.png
--------------------------------------------------------------------------------
/internal/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Swagger UI
7 |
8 |
9 |
10 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/internal/dist/oauth2-redirect.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Swagger UI: OAuth2 Redirect
5 |
6 |
7 |
8 |
9 |
76 |
--------------------------------------------------------------------------------
/internal/dist/openapi.yaml.example:
--------------------------------------------------------------------------------
1 | openapi: 3.0.1
2 | info:
3 | title: Build OAS3.0.1
4 | description: Description - Builder Testing for OAS3.0.1
5 | termsOfService: https://smartbear.com/terms-of-use/
6 | contact:
7 | email: aleksandar.nesovic@protonmail.com
8 | license:
9 | name: MIT
10 | url: https://github.com/go-oas/docs/blob/main/LICENSE
11 | version: 1.0.1
12 | externalDocs:
13 | description: External documentation
14 | url: https://kaynetik.com
15 | servers:
16 | - url: https://petstore.swagger.io/v2
17 | - url: http://petstore.swagger.io/v2
18 | tags:
19 | - name: user
20 | description: Operations about the User
21 | externalDocs:
22 | description: User from the Petstore example
23 | url: http://swagger.io
24 | - name: pet
25 | description: Everything about your Pets
26 | externalDocs:
27 | description: Find out more about our store (Swagger UI Example)
28 | url: http://swagger.io
29 | - name: petko
30 | description: Everything about your Petko
31 | externalDocs:
32 | description: Find out more about our store (Swagger UI Example)
33 | url: http://swagger.io
34 | paths:
35 | /users:
36 | get:
37 | operationId: getUser
38 | requestBody:
39 | content: {}
40 | description: ""
41 | responses:
42 | 200:
43 | content: {}
44 | description: OK
45 | security: []
46 | summary: Get a User
47 | tags:
48 | - pet
49 | post:
50 | operationId: createUser
51 | requestBody:
52 | content:
53 | application/json:
54 | schema:
55 | $ref: '#/components/schemas/User'
56 | description: Create a new User
57 | responses:
58 | 200:
59 | content: {}
60 | description: OK
61 | 404:
62 | content:
63 | application/json:
64 | schema:
65 | $ref: '#/components/schemas/User'
66 | description: Not Found
67 | security:
68 | - petstore_auth:
69 | - write:users
70 | - read:users
71 | summary: Create a new User
72 | tags:
73 | - user
74 | components:
75 | schemas:
76 | ApiResponse:
77 | $ref: ""
78 | properties:
79 | code:
80 | format: int32
81 | type: integer
82 | message:
83 | type: string
84 | type:
85 | type: string
86 | type: object
87 | xml:
88 | name: ApiResponse
89 | Tag:
90 | $ref: ""
91 | properties:
92 | id:
93 | format: int64
94 | type: integer
95 | name:
96 | type: string
97 | type: object
98 | xml:
99 | name: Tag
100 | User:
101 | $ref: ""
102 | properties:
103 | email:
104 | type: string
105 | id:
106 | description: UserID
107 | format: int64
108 | type: integer
109 | phForEnums:
110 | enum:
111 | - placed
112 | - approved
113 | type: enum
114 | userStatus:
115 | description: User Status
116 | format: int32
117 | type: integer
118 | username:
119 | type: string
120 | type: object
121 | xml:
122 | name: User
123 | securitySchemes:
124 | api_key:
125 | in: header
126 | name: api_key
127 | type: apiKey
128 | petstore_auth:
129 | flows:
130 | implicit:
131 | authorizationUrl: http://petstore.swagger.io/oauth/dialog
132 | scopes:
133 | read:users: Read users
134 | write:users: Modify users
135 | type: oauth2
136 |
--------------------------------------------------------------------------------
/models.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | // WARNING:
4 | // Most structures in here are an representation of what is defined in default
5 | // Open API Specification documentation, v3.0.3.
6 | //
7 | // [More about it can be found on this link](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md)
8 |
9 | // New returns a new instance of OAS structure.
10 | func New() OAS {
11 | initRoutes := RegRoutes{}
12 |
13 | return OAS{
14 | RegisteredRoutes: initRoutes,
15 | }
16 | }
17 |
18 | const (
19 | oasAnnotationInit = "// @OAS "
20 | )
21 |
22 | // OAS - represents Open API Specification structure, in its approximated Go form.
23 | type OAS struct {
24 | OASVersion OASVersion `yaml:"openapi"`
25 | Info Info `yaml:"info"`
26 | ExternalDocs ExternalDocs `yaml:"externalDocs"`
27 | Servers Servers `yaml:"servers"`
28 | Tags Tags `yaml:"tags"`
29 | Paths Paths `yaml:"paths"`
30 | Components Components `yaml:"components"`
31 | RegisteredRoutes RegRoutes `yaml:"-"`
32 | }
33 |
34 | type (
35 | // Version represents a SemVer version.
36 | Version string
37 |
38 | // URL represents and URL which is casted from string.
39 | URL string
40 |
41 | // OASVersion represents the OpenAPISpecification version which will be used.
42 | OASVersion Version
43 | )
44 |
45 | // Info represents OAS info object.
46 | type Info struct {
47 | Title string `yaml:"title"`
48 | Description string `yaml:"description"`
49 | TermsOfService URL `yaml:"termsOfService"`
50 | Contact Contact `yaml:"contact"`
51 | License License `yaml:"license"`
52 | Version Version `yaml:"version"`
53 | }
54 |
55 | // Contact represents OAS contact object, used by Info.
56 | type Contact struct {
57 | Email string `yaml:"email"`
58 | }
59 |
60 | // License represents OAS license object, used by Info.
61 | type License struct {
62 | Name string `yaml:"name"`
63 | URL URL `yaml:"url"`
64 | }
65 |
66 | // ExternalDocs represents OAS externalDocs object.
67 | //
68 | // Aside from base OAS structure, this is also used by Tag object.
69 | type ExternalDocs struct {
70 | Description string `yaml:"description"`
71 | URL URL `yaml:"url"`
72 | }
73 |
74 | // Servers is a slice of Server objects.
75 | type Servers []Server
76 |
77 | // Server represents OAS server object.
78 | type Server struct {
79 | URL URL `yaml:"url"`
80 | }
81 |
82 | // Tags is a slice of Tag objects.
83 | type Tags []Tag
84 |
85 | // Tag represents OAS tag object.
86 | type Tag struct {
87 | Name string `yaml:"name"`
88 | Description string `yaml:"description"`
89 | ExternalDocs ExternalDocs `yaml:"externalDocs"`
90 | }
91 |
92 | // Paths is a slice of Path objects.
93 | type Paths []Path
94 |
95 | // Path represents OAS path object.
96 | type Path struct {
97 | Route string `yaml:"route"`
98 | HTTPMethod string `yaml:"httpMethod"`
99 | Tags []string `yaml:"tags"`
100 | Summary string `yaml:"summary"`
101 | Description string `yaml:"description"`
102 | OperationID string `yaml:"operationId"`
103 | RequestBody RequestBody `yaml:"requestBody"`
104 | Responses Responses `yaml:"responses"`
105 | Security SecurityEntities `yaml:"security,omitempty"`
106 | Parameters Parameters `yaml:"parameters,omitempty"`
107 | HandlerFuncName string `yaml:"-"`
108 | }
109 |
110 | // RequestBody represents OAS requestBody object, used by Path.
111 | type RequestBody struct {
112 | Description string `yaml:"description"`
113 | Content ContentTypes `yaml:"content"`
114 | Required bool `yaml:"required"`
115 | }
116 |
117 | // ContentTypes is a slice of ContentType objects.
118 | type ContentTypes []ContentType
119 |
120 | // ContentType represents OAS content type object, used by RequestBody and Response.
121 | type ContentType struct {
122 | Name string `yaml:"ct-name"` // e.g. application/json
123 | Schema string `yaml:"ct-schema"` // e.g. $ref: '#/components/schemas/Pet'
124 | }
125 |
126 | // Responses is a slice of Response objects.
127 | type Responses []Response
128 |
129 | // Response represents OAS response object, used by Path.
130 | type Response struct {
131 | Code uint `yaml:"code"`
132 | Description string `yaml:"description"`
133 | Content ContentTypes `yaml:"content"`
134 | }
135 |
136 | // SecurityEntities is a slice of Security objects.
137 | type SecurityEntities []Security
138 |
139 | // Security represents OAS security object.
140 | type Security struct {
141 | AuthName string
142 | PermTypes []string // write:pets , read:pets etc.
143 | }
144 |
145 | // Components is a slice of Component objects.
146 | type Components []Component
147 |
148 | // Component represents OAS component object.
149 | type Component struct {
150 | Schemas Schemas `yaml:"schemas"`
151 | SecuritySchemes SecuritySchemes `yaml:"securitySchemes"`
152 | }
153 |
154 | // Schemas is a slice of Schema objects.
155 | type Schemas []Schema
156 |
157 | // Schema represents OAS schema object, used by Component.
158 | type Schema struct {
159 | Name string
160 | Type string
161 | Properties SchemaProperties
162 | XML XMLEntry `yaml:"xml, omitempty"`
163 | Ref string // $ref: '#/components/schemas/Pet' // TODO: Should this be omitted if empty?
164 | }
165 |
166 | // XMLEntry represents name of XML entry in Schema object.
167 | type XMLEntry struct {
168 | Name string
169 | }
170 |
171 | // SchemaProperties is a slice of SchemaProperty objects.
172 | type SchemaProperties []SchemaProperty
173 |
174 | // SchemaProperty represents OAS schema object, used by Schema.
175 | type SchemaProperty struct {
176 | Name string `yaml:"-"`
177 | Type string // OAS3.0 data types - e.g. integer, boolean, string
178 | Format string `yaml:"format,omitempty"`
179 | Description string `yaml:"description,omitempty"`
180 | Enum []string `yaml:"enum,omitempty"`
181 | Default interface{} `yaml:"default,omitempty"`
182 | }
183 |
184 | // SecuritySchemes is a slice of SecuritySchemes objects.
185 | type SecuritySchemes []SecurityScheme
186 |
187 | // SecurityScheme represents OAS security object, used by Component.
188 | type SecurityScheme struct {
189 | Name string `yaml:"name,omitempty"`
190 | Type string `yaml:"type,omitempty"`
191 | In string `yaml:"in,omitempty"`
192 | Flows SecurityFlows `yaml:"flows,omitempty"`
193 | }
194 |
195 | // SecurityFlows is a slice of SecurityFlow objects.
196 | type SecurityFlows []SecurityFlow
197 |
198 | // SecurityFlow represents OAS Flows object, used by SecurityScheme.
199 | type SecurityFlow struct {
200 | Type string `yaml:"type,omitempty"`
201 | AuthURL URL `yaml:"authorizationUrl,omitempty"`
202 | Scopes SecurityScopes `yaml:"scopes,omitempty"`
203 | }
204 |
205 | // SecurityScopes is a slice of SecurityScope objects.
206 | type SecurityScopes []SecurityScope
207 |
208 | // SecurityScope represents OAS SecurityScope object, used by SecurityFlow.
209 | type SecurityScope struct {
210 | Name string `yaml:"name,omitempty"`
211 | Description string `yaml:"description,omitempty"`
212 | }
213 |
214 | // Parameters is a slice of Parameter objects.
215 | type Parameters []Parameter
216 |
217 | // Parameter represents OAS parameter object.
218 | type Parameter struct {
219 | // If in is "path", the name field MUST correspond to a template expression occurring within
220 | // the path field in the Paths Object. See Path Templating for further information.
221 | // If in is "header" and the name field is "Accept", "Content-Type" or "Authorization",
222 | // the parameter definition SHALL be ignored.
223 | // For all other cases, the name corresponds to the parameter name used by the in property.
224 | Name string `yaml:"name,omitempty"`
225 | In string `yaml:"in,omitempty"` // "query", "header", "path" or "cookie".
226 | Description string `yaml:"description,omitempty"`
227 | Required bool `yaml:"required,omitempty"`
228 | Deprecated bool `yaml:"deprecated,omitempty"`
229 | AllowEmptyValue bool `yaml:"allowEmptyValue,omitempty"`
230 | Schema Schema
231 | }
232 |
233 | // isEmpty checks if *ExternalDocs struct is empty.
234 | func (ed *ExternalDocs) isEmpty() bool {
235 | if ed == nil {
236 | return true
237 | }
238 |
239 | if ed.Description == emptyStr && ed.URL == emptyStr {
240 | return true
241 | }
242 |
243 | return false
244 | }
245 |
--------------------------------------------------------------------------------
/models_test.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestUnitNew(t *testing.T) {
9 | t.Parallel()
10 |
11 | got := New()
12 |
13 | want := OAS{
14 | RegisteredRoutes: RegRoutes{},
15 | }
16 |
17 | if !reflect.DeepEqual(got, want) {
18 | t.Errorf("got %+v, but want %+v", got, want)
19 | }
20 | }
21 |
22 | func TestUnitEDIsEmpty(t *testing.T) {
23 | t.Parallel()
24 |
25 | edNil := (*ExternalDocs)(nil)
26 | if !edNil.isEmpty() {
27 | t.Errorf("expected an empty external docs struct")
28 | }
29 |
30 | edEmpty := ExternalDocs{}
31 | if !edEmpty.isEmpty() {
32 | t.Error("expected an empty external docs struct")
33 | }
34 |
35 | edEmptyURL := ExternalDocs{
36 | Description: "description",
37 | }
38 |
39 | if !edEmptyURL.isEmpty() && !isStrEmpty(string(edEmptyURL.URL)) {
40 | t.Errorf("expected an empty URL")
41 | }
42 |
43 | edEmptyDesc := ExternalDocs{
44 | URL: "description",
45 | }
46 |
47 | if !edEmptyDesc.isEmpty() && !isStrEmpty(edEmptyDesc.Description) {
48 | t.Errorf("expected an empty Description")
49 | }
50 |
51 | ed := ExternalDocs{
52 | Description: "to many to describe",
53 | URL: "Gagarin URI",
54 | }
55 |
56 | if ed.isEmpty() &&
57 | !isStrEmpty(ed.Description) &&
58 | !isStrEmpty(string(ed.URL)) {
59 | t.Errorf("expected complete ExternalDocs struct")
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/routing.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | import (
4 | "reflect"
5 | "runtime"
6 | "strings"
7 | )
8 |
9 | type (
10 | // RouteFn represents a typeFunc which needs to be satisfied in order to use default routes attaching method.
11 | RouteFn func(index int, oas *OAS)
12 |
13 | // RegRoutes represent a map of RouteFn's.
14 | //
15 | // Note: Considering to un-export it.
16 | RegRoutes map[string]RouteFn
17 | )
18 |
19 | // AttachRoutes if used for attaching pre-defined API documentation routes.
20 | //
21 | // fns param is a slice of functions that satisfy RouteFn signature.
22 | func (oas *OAS) AttachRoutes(fns []RouteFn) {
23 | for _, fn := range fns {
24 | fnDeclaration := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
25 | fields := strings.SplitAfter(fnDeclaration, ".")
26 | fnName := fields[len(fields)-1]
27 |
28 | oas.RegisteredRoutes[fnName] = fn
29 | }
30 | }
31 |
32 | // GetRegisteredRoutes returns a map of registered RouteFn functions - in layman terms "routes".
33 | func (oas *OAS) GetRegisteredRoutes() RegRoutes {
34 | return oas.RegisteredRoutes
35 | }
36 |
37 | // GetPathByIndex returns ptr to Path structure, by its index in the parent struct of OAS.
38 | func (oas *OAS) GetPathByIndex(index int) *Path {
39 | return &oas.Paths[index]
40 | }
41 |
42 | // AddRoute is used for add API documentation routes.
43 | func (oas *OAS) AddRoute(path *Path) {
44 | oas.Paths = append(oas.Paths, *path)
45 | }
46 |
--------------------------------------------------------------------------------
/routing_test.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | import (
4 | "math/rand"
5 | "net/http"
6 | "reflect"
7 | "runtime"
8 | "strings"
9 | "testing"
10 | "testing/quick"
11 | )
12 |
13 | func fetchRegRoutes(t *testing.T, count int) RegRoutes {
14 | t.Helper()
15 |
16 | routes := make(RegRoutes)
17 | randStr := RandomString(t, count)
18 |
19 | ir := initRoutes(t, count)
20 |
21 | for i := 0; i < count; i++ {
22 | routes[randStr] = ir[i]
23 | }
24 |
25 | return routes
26 | }
27 |
28 | func initRoutes(t *testing.T, count int) []RouteFn {
29 | t.Helper()
30 |
31 | var routes []RouteFn
32 |
33 | for i := 0; i < count; i++ {
34 | tmpFn := func(i int, o *OAS) {}
35 | routes = append(routes, tmpFn)
36 | }
37 |
38 | return routes
39 | }
40 |
41 | func TestUnitGetRegisteredRoutes(t *testing.T) {
42 | t.Parallel()
43 |
44 | type fields struct {
45 | registeredRoutes RegRoutes
46 | }
47 |
48 | v := rand.Intn(1000-1) + 1 //nolint:gosec //ignored in tests.
49 | regRoutes := fetchRegRoutes(t, v)
50 |
51 | type st struct {
52 | name string
53 | fields fields
54 | want RegRoutes
55 | }
56 |
57 | tests := []st{
58 | {
59 | name: "success getting registered routes",
60 | fields: fields{
61 | registeredRoutes: regRoutes,
62 | },
63 | want: regRoutes,
64 | },
65 | }
66 | for _, tt := range tests {
67 | stin := tt
68 |
69 | t.Run(stin.name, func(t *testing.T) {
70 | t.Parallel()
71 |
72 | o := &OAS{
73 | RegisteredRoutes: stin.fields.registeredRoutes,
74 | }
75 | got := o.GetRegisteredRoutes()
76 | want := stin.want
77 |
78 | if !reflect.DeepEqual(got, want) {
79 | t.Errorf("GetRegisteredRoutes() = %v, want %v", got, want)
80 | }
81 | })
82 | }
83 | }
84 |
85 | func TestUnitGetPathByIndex(t *testing.T) {
86 | t.Parallel()
87 |
88 | type fields struct {
89 | Paths Paths
90 | registeredRoutes RegRoutes
91 | }
92 |
93 | type st struct {
94 | name string
95 | fields fields
96 | want *Path
97 | }
98 |
99 | paths := Paths{
100 | Path{
101 | Route: "/test",
102 | HTTPMethod: http.MethodGet,
103 | },
104 | }
105 |
106 | v := rand.Intn(1000-1) + 1 //nolint:gosec //ignored in tests.
107 | regRoutes := fetchRegRoutes(t, v)
108 |
109 | tests := []st{
110 | {
111 | name: "success get paths",
112 | fields: fields{
113 | Paths: paths,
114 | registeredRoutes: regRoutes,
115 | },
116 | want: &paths[0],
117 | },
118 | }
119 | for _, tt := range tests {
120 | stin := tt
121 | t.Run(stin.name, func(t *testing.T) {
122 | t.Parallel()
123 |
124 | o := &OAS{
125 | Paths: stin.fields.Paths,
126 | RegisteredRoutes: stin.fields.registeredRoutes,
127 | }
128 | if got := o.GetPathByIndex(0); !reflect.DeepEqual(got, stin.want) {
129 | t.Errorf("GetPathByIndex() = %v, want %v", got, stin.want)
130 | }
131 | })
132 | }
133 | }
134 |
135 | func TestUnitAttachRoutes(t *testing.T) {
136 | t.Parallel()
137 |
138 | rr := make(RegRoutes)
139 |
140 | o := OAS{
141 | RegisteredRoutes: rr,
142 | }
143 |
144 | routes := initRoutes(t, 5)
145 |
146 | o.AttachRoutes(routes)
147 |
148 | for _, routeFn := range routes {
149 | fnDeclaration := runtime.FuncForPC(reflect.ValueOf(routeFn).Pointer()).Name()
150 | fields := strings.SplitAfter(fnDeclaration, ".")
151 | fnName := fields[len(fields)-1]
152 | got := o.RegisteredRoutes[fnName]
153 |
154 | if reflect.ValueOf(got) != reflect.ValueOf(routeFn) {
155 | t.Errorf("invalid route fn attached: got %v, want %v", got, routeFn)
156 | }
157 | }
158 | }
159 |
160 | func TestQuickUnitGetRegisteredRoutes(t *testing.T) {
161 | t.Parallel()
162 |
163 | config := quick.Config{
164 | Values: func(args []reflect.Value, rand *rand.Rand) {
165 | oas := OAS{
166 | RegisteredRoutes: map[string]RouteFn{},
167 | }
168 | args[0] = reflect.ValueOf(oas)
169 | },
170 | }
171 |
172 | gotRegRoutes := func(oas OAS) bool {
173 | got := oas.GetRegisteredRoutes()
174 |
175 | return reflect.DeepEqual(got, oas.RegisteredRoutes)
176 | }
177 |
178 | if err := quick.Check(gotRegRoutes, &config); err != nil {
179 | t.Errorf("Check failed: %#v", err)
180 | }
181 | }
182 |
183 | func getPaths(t *testing.T, count int, rndStr string) Paths {
184 | t.Helper()
185 |
186 | pt := make(Paths, 0, count+2)
187 |
188 | for i := 0; i < count+1; i++ {
189 | pt = append(pt, Path{
190 | Route: rndStr,
191 | HTTPMethod: http.MethodGet,
192 | HandlerFuncName: rndStr,
193 | })
194 | }
195 |
196 | return pt
197 | }
198 |
199 | func TestQuickUnitGetPathByIndex(t *testing.T) {
200 | t.Parallel()
201 |
202 | config := quick.Config{
203 | Values: func(args []reflect.Value, rand *rand.Rand) {
204 | count := rand.Intn(550-1) + 1
205 | oas := OAS{
206 | Paths: getPaths(t, count, RandomString(t, count)),
207 | }
208 |
209 | args[0] = reflect.ValueOf(oas)
210 | },
211 | }
212 |
213 | gotRegRoutes := func(oas OAS) bool {
214 | pathsLen := len(oas.Paths)
215 | r := 2
216 |
217 | if pathsLen > 3 {
218 | r = int(uint(len(oas.Paths) - 2))
219 | }
220 |
221 | upRnd := int(uint(rand.Intn(r))) //nolint:gosec //week rnd generator - ignore in test.
222 |
223 | randIndex := uint(pathsLen - upRnd)
224 |
225 | got := oas.GetPathByIndex(int(randIndex - 1))
226 |
227 | return reflect.DeepEqual(got, &oas.Paths[randIndex-1])
228 | }
229 |
230 | if err := quick.Check(gotRegRoutes, &config); err != nil {
231 | t.Errorf("Check failed: %#v", err)
232 | }
233 | }
234 |
235 | func RandomString(t *testing.T, n int) string {
236 | t.Helper()
237 |
238 | letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
239 |
240 | s := make([]rune, n)
241 | for i := range s {
242 | s[i] = letters[rand.Intn(len(letters))] //nolint:gosec //ignored in tests.
243 | }
244 |
245 | return string(s)
246 | }
247 |
248 | func TestQuickUnitAttachRoutes(t *testing.T) {
249 | t.Parallel()
250 |
251 | config := quick.Config{
252 | Values: func(args []reflect.Value, rand *rand.Rand) {
253 | rr := make(RegRoutes)
254 |
255 | oas := OAS{
256 | RegisteredRoutes: rr,
257 | }
258 |
259 | routes := initRoutes(t, 5)
260 |
261 | args[0] = reflect.ValueOf(oas)
262 | args[1] = reflect.ValueOf(routes)
263 | },
264 | }
265 |
266 | gotRegRoutes := func(oas OAS, routes []RouteFn) bool {
267 | oas.AttachRoutes(routes)
268 | got := oas.GetRegisteredRoutes()
269 |
270 | return reflect.DeepEqual(got, oas.RegisteredRoutes)
271 | }
272 |
273 | if err := quick.Check(gotRegRoutes, &config); err != nil {
274 | t.Errorf("Check failed: %#v", err)
275 | }
276 | }
277 |
278 | func TestOAS_AddRoute(t *testing.T) {
279 | t.Parallel()
280 |
281 | var (
282 | respose200 = Response{Code: 200, Description: "Ok"}
283 | respose404 = Response{Code: 404, Description: "Not Found"}
284 | contentTypeUser = ContentType{Name: "application/json", Schema: "#/components/schemas/User"}
285 | requestBodyGetUser = RequestBody{
286 | Description: "Get a User",
287 | Content: ContentTypes{contentTypeUser},
288 | Required: true,
289 | }
290 | requestBodyCreateUser = RequestBody{
291 | Description: "Create a new User",
292 | Content: ContentTypes{contentTypeUser},
293 | Required: true,
294 | }
295 | pathGetUser = Path{
296 | Route: "/users",
297 | HTTPMethod: "GET",
298 | OperationID: "getUser",
299 | Summary: "Get a User",
300 | Responses: Responses{respose200},
301 | RequestBody: requestBodyGetUser,
302 | }
303 | pathCreateUser = Path{
304 | Route: "/users",
305 | HTTPMethod: "POST",
306 | OperationID: "createUser",
307 | Summary: "Create a new User",
308 | Responses: Responses{respose200, respose404},
309 | RequestBody: requestBodyCreateUser,
310 | }
311 | )
312 |
313 | tests := []struct {
314 | name string
315 | oas *OAS
316 | path *Path
317 | wantPaths Paths
318 | }{
319 | {
320 | name: "success-no-existing-paths",
321 | oas: &OAS{},
322 | path: &pathGetUser,
323 | wantPaths: Paths{pathGetUser},
324 | },
325 | {
326 | name: "success-existing-paths",
327 | oas: &OAS{Paths: Paths{pathGetUser}},
328 | path: &pathCreateUser,
329 | wantPaths: Paths{pathGetUser, pathCreateUser},
330 | },
331 | }
332 |
333 | for _, tt := range tests {
334 | trn := tt
335 |
336 | t.Run(trn.name, func(t *testing.T) {
337 | t.Parallel()
338 |
339 | trn.oas.AddRoute(trn.path)
340 | if !reflect.DeepEqual(trn.wantPaths, trn.oas.Paths) {
341 | t.Errorf("OAS.AddRoute() = [%v], want {%v}", trn.oas.Paths, trn.wantPaths)
342 | }
343 | })
344 | }
345 | }
346 |
--------------------------------------------------------------------------------
/server.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "log"
8 | "net/http"
9 | "os"
10 | "strings"
11 | "syscall"
12 | "time"
13 | )
14 |
15 | const (
16 | defaultRoute = "/api"
17 | defaultDirectory = "./internal/dist"
18 | defaultIndexPath = "/index.html"
19 | fwSlashSuffix = "/"
20 | sigContSleeperMilliseconds = 20
21 | readHeaderTimeout = 60 * time.Second // same than nginx
22 | )
23 |
24 | // ConfigSwaggerUI represents a structure which will be used to pass required configuration params to
25 | //
26 | // the ServeSwaggerUI func.
27 | type ConfigSwaggerUI struct {
28 | Route string
29 | Port string
30 |
31 | httpServer *http.Server
32 | initFS fileSystem
33 | stopper chan os.Signal
34 | }
35 |
36 | // ServeSwaggerUI does what its name implies - runs Swagger UI on mentioned set port and route.
37 | func ServeSwaggerUI(conf *ConfigSwaggerUI) error {
38 | if conf == nil {
39 | return errors.New("swagger config is required")
40 | }
41 |
42 | if conf.Route == "" {
43 | conf.Route = defaultRoute
44 | }
45 |
46 | if conf.initFS.isNil() {
47 | conf.initFS = *initializeStandardFS()
48 | }
49 |
50 | if conf.httpServer == nil {
51 | conf.initializeDefaultHTTPServer()
52 | }
53 |
54 | log.Printf("Serving SwaggerIU on HTTP port: %s\n", conf.Port)
55 | conf.sigCont()
56 |
57 | val := <-conf.stopper
58 |
59 | switch val {
60 | case syscall.SIGINT, syscall.SIGKILL:
61 | log.Printf("SwaggerUI did not shut down properly: %v", conf.httpServer.Shutdown(context.Background()))
62 | default:
63 | log.Printf("SwaggerUI server experienced an unexpected error: %v", conf.httpServer.ListenAndServe())
64 | }
65 |
66 | return nil
67 | }
68 |
69 | // fileSystem represents a wrapper for http.FileSystem, with relevant type func implementations.
70 | type fileSystem struct {
71 | fileSysInit http.FileSystem
72 |
73 | fsOpenFn fsOpenFn
74 | getStatFn getStatFn
75 | getIsDir getIsDirFn
76 | }
77 |
78 | type (
79 | fsOpenFn func(name string) (http.File, error)
80 | fsIsDirFn func() bool
81 | fileStatFn func() (os.FileInfo, error)
82 | getStatFn func(file http.File) fileStatFn
83 | getIsDirFn func(file os.FileInfo) fsIsDirFn
84 | )
85 |
86 | func initializeStandardFS() *fileSystem {
87 | fsInit := http.Dir(defaultDirectory)
88 |
89 | return &fileSystem{
90 | fileSysInit: fsInit,
91 | fsOpenFn: newFSOpen(fsInit),
92 | getStatFn: newGetStatFn(),
93 | getIsDir: newGetIsDirFn(),
94 | }
95 | }
96 |
97 | func newFSOpen(fis http.FileSystem) fsOpenFn {
98 | return func(name string) (http.File, error) {
99 | return fis.Open(name)
100 | }
101 | }
102 |
103 | func newGetStatFn() getStatFn {
104 | return func(file http.File) fileStatFn {
105 | return func() (os.FileInfo, error) {
106 | return file.Stat()
107 | }
108 | }
109 | }
110 |
111 | func newGetIsDirFn() getIsDirFn {
112 | return func(file os.FileInfo) fsIsDirFn {
113 | return func() bool {
114 | return file.IsDir()
115 | }
116 | }
117 | }
118 |
119 | func (fis *fileSystem) isNil() bool {
120 | if fis == nil {
121 | return true
122 | }
123 |
124 | if fis.getStatFn == nil ||
125 | fis.getIsDir == nil ||
126 | fis.fsOpenFn == nil ||
127 | fis.fileSysInit == nil {
128 | return true
129 | }
130 |
131 | return false
132 | }
133 |
134 | // Open opens file. Returns http.File, and error if there is any.
135 | func (fis fileSystem) Open(path string) (http.File, error) {
136 | f, err := fis.fsOpenFn(path)
137 | if err != nil {
138 | return nil, fmt.Errorf("failed to open file in path %s :%w", path, err)
139 | }
140 |
141 | fileInfo, err := fis.getStatFn(f)()
142 | if err != nil {
143 | return f, fmt.Errorf("failed to fetch file info :%w", err)
144 | }
145 |
146 | if fis.getIsDir(fileInfo)() {
147 | index := strings.TrimSuffix(path, fwSlashSuffix) + defaultIndexPath
148 | if _, err = fis.fileSysInit.Open(index); err != nil {
149 | return nil, fmt.Errorf("failed trimming path sufix :%w", err)
150 | }
151 | }
152 |
153 | return f, nil
154 | }
155 |
156 | func (c *ConfigSwaggerUI) initializeDefaultHTTPServer() {
157 | fileServer := http.FileServer(c.initFS)
158 |
159 | c.httpServer = &http.Server{
160 | Addr: fmt.Sprintf(":%s", c.Port),
161 | Handler: http.StripPrefix(strings.TrimRight(c.Route, fwSlashSuffix), fileServer),
162 | ReadHeaderTimeout: readHeaderTimeout,
163 | }
164 | }
165 |
166 | func (c *ConfigSwaggerUI) sigCont() {
167 | if c.stopper == nil {
168 | osSignal := make(chan os.Signal)
169 | c.stopper = osSignal
170 |
171 | go func() {
172 | time.Sleep(sigContSleeperMilliseconds * time.Millisecond)
173 | osSignal <- syscall.SIGCONT
174 | }()
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/server_test.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | import (
4 | "errors"
5 | "math/rand"
6 | "net/http"
7 | "os"
8 | "reflect"
9 | "syscall"
10 | "testing"
11 | "testing/quick"
12 | "time"
13 | )
14 |
15 | func TestUnitQuickInitializeStandardFS(t *testing.T) {
16 | t.Parallel()
17 |
18 | initStandardFS := func() bool {
19 | fsInit := http.Dir(defaultDirectory)
20 | want := fileSystem{
21 | fileSysInit: fsInit,
22 | fsOpenFn: newFSOpen(fsInit),
23 | getStatFn: newGetStatFn(),
24 | getIsDir: newGetIsDirFn(),
25 | }
26 |
27 | got := *initializeStandardFS()
28 |
29 | if got.fileSysInit != want.fileSysInit {
30 | return false
31 | }
32 |
33 | if reflect.TypeOf(got.fsOpenFn) != reflect.TypeOf(want.fsOpenFn) {
34 | return false
35 | }
36 |
37 | if reflect.TypeOf(got.getStatFn) != reflect.TypeOf(want.getStatFn) {
38 | return false
39 | }
40 |
41 | if reflect.TypeOf(got.getIsDir) != reflect.TypeOf(want.getIsDir) {
42 | return false
43 | }
44 |
45 | return true
46 | }
47 | if err := quick.Check(initStandardFS, nil); err != nil {
48 | t.Errorf("Check failed: %#v", err)
49 | }
50 | }
51 |
52 | func TestUnitQuickInitializeStandardFSErr(t *testing.T) {
53 | t.Parallel()
54 |
55 | config := quick.Config{
56 | Values: func(values []reflect.Value, rand *rand.Rand) {
57 | fis := http.Dir(defaultDirectory)
58 | testFsOpenFn := newFSOpen(fis)
59 | testGetStatFn := newGetStatFn()
60 | testGetIsDirFn := newGetIsDirFn()
61 |
62 | values[0] = reflect.ValueOf(testFsOpenFn)
63 | values[1] = reflect.ValueOf(testGetStatFn)
64 | values[2] = reflect.ValueOf(testGetIsDirFn)
65 | },
66 | }
67 |
68 | initStandardFS := func(fsOpenFn fsOpenFn, getStatFn getStatFn, getIsDirFn getIsDirFn) bool {
69 | fsInit := http.Dir(defaultDirectory)
70 | want := fileSystem{
71 | fileSysInit: fsInit,
72 | fsOpenFn: fsOpenFn,
73 | getStatFn: getStatFn,
74 | getIsDir: getIsDirFn,
75 | }
76 |
77 | got := *initializeStandardFS()
78 |
79 | if got.fileSysInit != want.fileSysInit {
80 | return false
81 | }
82 |
83 | if reflect.TypeOf(got.fsOpenFn) != reflect.TypeOf(want.fsOpenFn) {
84 | return false
85 | }
86 |
87 | if reflect.TypeOf(got.getStatFn) != reflect.TypeOf(want.getStatFn) {
88 | return false
89 | }
90 |
91 | if reflect.TypeOf(got.getIsDir) != reflect.TypeOf(want.getIsDir) {
92 | return false
93 | }
94 |
95 | return true
96 | }
97 | if err := quick.Check(initStandardFS, &config); err != nil {
98 | t.Errorf("Check failed: %#v", err)
99 | }
100 | }
101 |
102 | func TestUnitNewFSOpen(t *testing.T) {
103 | t.Parallel()
104 |
105 | fsInit := http.Dir(defaultDirectory)
106 |
107 | got := newFSOpen(fsInit)
108 | want := func(name string) (http.File, error) {
109 | return fsInit.Open(name)
110 | }
111 |
112 | gotRes, _ := got("/")
113 | wantRes, _ := want("/")
114 |
115 | gstat, _ := gotRes.Stat()
116 | wstat, _ := wantRes.Stat()
117 |
118 | if !reflect.DeepEqual(gstat, wstat) {
119 | t.Error()
120 | }
121 | }
122 |
123 | func TestUnitIsFSNil(t *testing.T) {
124 | t.Parallel()
125 |
126 | nilFS := (*fileSystem)(nil)
127 | if !nilFS.isNil() {
128 | t.Error()
129 | }
130 |
131 | fsInit := fileSystem{}
132 | if !fsInit.isNil() {
133 | t.Error()
134 | }
135 |
136 | fsStandard := *initializeStandardFS()
137 | if fsStandard.isNil() {
138 | t.Error()
139 | }
140 | }
141 |
142 | func TestUnitFSOpen(t *testing.T) {
143 | t.Parallel()
144 |
145 | fsStd := *initializeStandardFS()
146 | if fsStd.isNil() {
147 | t.Error()
148 | }
149 |
150 | file, err := fsStd.Open("")
151 | if err != nil {
152 | t.Errorf("got an unexpected error: %v", err)
153 | }
154 |
155 | if file == nil {
156 | t.Error("file is expected not to be nil")
157 | }
158 |
159 | fstat, _ := file.Stat()
160 | if fstat.Name() != "dist" {
161 | t.Errorf("got an unexpected file name: %v", fstat.Name())
162 | }
163 | }
164 |
165 | func TestUnitFSOpenOpenErr(t *testing.T) {
166 | t.Parallel()
167 |
168 | fsInit := http.Dir(defaultDirectory)
169 |
170 | fsStd := &fileSystem{
171 | fileSysInit: fsInit,
172 | fsOpenFn: errFSOpen(t, fsInit),
173 | getStatFn: newGetStatFn(),
174 | getIsDir: newGetIsDirFn(),
175 | }
176 |
177 | if fsStd.isNil() {
178 | t.Error()
179 | }
180 |
181 | _, err := fsStd.Open("")
182 | if err == nil {
183 | t.Error("expected an error, got none")
184 | }
185 | }
186 |
187 | func TestUnitFSOpenGetStatErr(t *testing.T) {
188 | t.Parallel()
189 |
190 | fsInit := http.Dir(defaultDirectory)
191 |
192 | fsStd := &fileSystem{
193 | fileSysInit: fsInit,
194 | fsOpenFn: newFSOpen(fsInit),
195 | getStatFn: errGetStatFn(t),
196 | getIsDir: newGetIsDirFn(),
197 | }
198 |
199 | if fsStd.isNil() {
200 | t.Error()
201 | }
202 |
203 | _, err := fsStd.Open("")
204 | if err == nil {
205 | t.Error("expected an error, got none")
206 | }
207 | }
208 |
209 | func TestUnitFSOpenDirInnerOpenErr(t *testing.T) {
210 | t.Parallel()
211 |
212 | fsInitCorrect := http.Dir(defaultDirectory)
213 | fsErr := http.Dir("!!\\@!")
214 |
215 | fsStd := &fileSystem{
216 | fileSysInit: fsErr,
217 | fsOpenFn: newFSOpen(fsInitCorrect),
218 | getStatFn: newGetStatFn(),
219 | getIsDir: statTrueGetIsDirFn(),
220 | }
221 |
222 | if fsStd.isNil() {
223 | t.Error()
224 | }
225 |
226 | _, err := fsStd.Open("")
227 | if err == nil {
228 | t.Error("expected an error, got none")
229 | }
230 | }
231 |
232 | func errFSOpen(t *testing.T, fis http.FileSystem) fsOpenFn {
233 | t.Helper()
234 |
235 | return func(name string) (http.File, error) {
236 | file, _ := fis.Open(name)
237 | return file, errors.New("triggerErr")
238 | }
239 | }
240 |
241 | func errGetStatFn(t *testing.T) getStatFn {
242 | t.Helper()
243 |
244 | return func(file http.File) fileStatFn {
245 | return func() (os.FileInfo, error) {
246 | fi, _ := file.Stat()
247 |
248 | return fi, errors.New("triggerErr")
249 | }
250 | }
251 | }
252 |
253 | func statTrueGetIsDirFn() getIsDirFn {
254 | return func(file os.FileInfo) fsIsDirFn {
255 | return func() bool {
256 | return true
257 | }
258 | }
259 | }
260 |
261 | func TestUnitSwaggerUIShutDown(t *testing.T) {
262 | t.Parallel()
263 |
264 | conf := (*ConfigSwaggerUI)(nil)
265 |
266 | err := ServeSwaggerUI(conf)
267 | if err == nil {
268 | t.Error("expected an error, got none")
269 | }
270 |
271 | osSignal := make(chan os.Signal)
272 |
273 | emptyRoute := &ConfigSwaggerUI{
274 | Route: "",
275 | stopper: osSignal,
276 | }
277 |
278 | go func() {
279 | time.Sleep(20 * time.Millisecond)
280 | osSignal <- syscall.SIGINT
281 | }()
282 |
283 | _ = ServeSwaggerUI(emptyRoute)
284 |
285 | if emptyRoute.Route != defaultRoute {
286 | t.Errorf("route wasn't altered from its zero state to default route")
287 | }
288 | }
289 |
290 | func TestUnitSigCont(t *testing.T) {
291 | t.Parallel()
292 |
293 | confSwg := &ConfigSwaggerUI{
294 | stopper: nil,
295 | }
296 |
297 | confSwg.sigCont()
298 |
299 | if confSwg.stopper == nil {
300 | t.Error("stopper chan is nil, should be os.Signal")
301 | }
302 | }
303 |
--------------------------------------------------------------------------------
/setters.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | // SetOASVersion sets the OAS version, by casting string to OASVersion type.
4 | func (oas *OAS) SetOASVersion(ver string) {
5 | oas.OASVersion = OASVersion(ver)
6 | }
7 |
8 | // GetInfo returns pointer to the Info struct.
9 | func (oas *OAS) GetInfo() *Info {
10 | return &oas.Info
11 | }
12 |
13 | // SetContact setts the contact on the Info struct.
14 | func (i *Info) SetContact(email string) {
15 | i.Contact = Contact{Email: email}
16 | }
17 |
18 | // SetLicense sets the license on the Info struct.
19 | func (i *Info) SetLicense(licType, url string) {
20 | i.License = License{
21 | Name: licType,
22 | URL: URL(url),
23 | }
24 | }
25 |
26 | // SetTag is used to define a new tag based on input params, and append it to the slice of tags its being called from.
27 | func (tt *Tags) SetTag(name, tagDescription string, extDocs ExternalDocs) {
28 | var tag Tag
29 |
30 | if !isStrEmpty(name) {
31 | tag.Name = name
32 | }
33 |
34 | if !isStrEmpty(tagDescription) {
35 | tag.Description = tagDescription
36 | }
37 |
38 | if !extDocs.isEmpty() {
39 | tag.ExternalDocs = extDocs
40 | }
41 |
42 | tt.AppendTag(&tag)
43 | }
44 |
45 | // AppendTag is used to append an Tag to the slice of Tags its being called from.
46 | func (tt *Tags) AppendTag(tag *Tag) {
47 | *tt = append(*tt, *tag)
48 | }
49 |
--------------------------------------------------------------------------------
/setters_test.go:
--------------------------------------------------------------------------------
1 | package docs
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestUnitInfoSetLicense(t *testing.T) {
9 | t.Parallel()
10 |
11 | info := Info{
12 | Title: "tester",
13 | }
14 | lic := "MIT"
15 | urlTest := "https://github.com/kaynetik"
16 | info.SetLicense(lic, urlTest)
17 |
18 | if info.License.Name != lic {
19 | t.Errorf("license name not set correctly")
20 | }
21 |
22 | if info.License.URL != URL(urlTest) {
23 | t.Error("license URL not set correctly")
24 | }
25 | }
26 |
27 | func TestUnitInfoGetInfo(t *testing.T) {
28 | t.Parallel()
29 |
30 | oasPrep := OAS{Info: Info{
31 | Title: testingPostfix,
32 | }}
33 |
34 | got := oasPrep.GetInfo()
35 |
36 | if !reflect.DeepEqual(got, &oasPrep.Info) {
37 | t.Errorf("failed getting OAS.Info reference")
38 | }
39 | }
40 |
41 | func TestUnitSetOASVersion(t *testing.T) {
42 | t.Parallel()
43 |
44 | oasPrep := OAS{Info: Info{
45 | Title: testingPostfix,
46 | }}
47 |
48 | verToSet := "3.1.1"
49 | oasPrep.SetOASVersion(verToSet)
50 |
51 | if oasPrep.OASVersion != OASVersion(verToSet) {
52 | t.Error("failed setting OAS.OASVersion")
53 | }
54 | }
55 |
56 | func TestUnitSetTag(t *testing.T) {
57 | t.Parallel()
58 |
59 | tags := Tags{}
60 | tName := "pettag"
61 | tDesc := "Everything about your Pets"
62 | tED := ExternalDocs{
63 | Description: "Find out more about our store (Swagger UI Example)",
64 | URL: "http://swagger.io",
65 | }
66 | tags.SetTag(tName, tDesc, tED)
67 |
68 | firstTag := tags[0]
69 | if firstTag.Name != tName ||
70 | firstTag.Description != tDesc ||
71 | firstTag.ExternalDocs != tED {
72 | t.Error("tag not set properly")
73 | }
74 | }
75 |
--------------------------------------------------------------------------------