├── .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 | ![GolangCI](https://github.com/go-oas/docs/workflows/golangci/badge.svg?branch=main) 8 | ![Build](https://github.com/go-oas/docs/workflows/Build/badge.svg?branch=main) 9 | [![Version](https://img.shields.io/badge/version-v1.0.5-success.svg)](https://github.com/go-oas/docs/releases) 10 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-oas/docs)](https://goreportcard.com/report/github.com/go-oas/docs) 11 | [![Coverage Status](https://coveralls.io/repos/github/go-oas/docs/badge.svg?branch=main)](https://coveralls.io/github/go-oas/docs?branch=main) 12 | [![codebeat badge](https://codebeat.co/badges/32b86556-84e3-4db9-9f11-923d12994f90)](https://codebeat.co/projects/github-com-go-oas-docs-main) 13 | [![Go Reference](https://pkg.go.dev/badge/github.com/go-oas/docs.svg)](https://pkg.go.dev/github.com/go-oas/docs) 14 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](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 | --------------------------------------------------------------------------------