├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── chai.go ├── chai ├── chai.go ├── chai_test.go ├── req_res_handler.go └── res_handler.go ├── chi ├── methods.go ├── openapi2.go ├── openapi2_test.go ├── openapi3.go └── testdata │ └── celler.json ├── examples ├── chi │ ├── basic │ │ └── main.go │ ├── celler │ │ ├── README.md │ │ └── main.go │ └── openapi3 │ │ ├── README.md │ │ └── main.go ├── docs │ ├── basic │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ ├── celler │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ └── openapi3 │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml ├── gorilla │ ├── basic │ │ └── main.go │ └── celler │ │ ├── README.md │ │ └── main.go └── shared │ ├── controller │ ├── accounts.go │ ├── admin.go │ ├── bottles.go │ ├── controller.go │ └── examples.go │ ├── httputil │ └── error.go │ └── model │ ├── account.go │ ├── admin.go │ ├── bottle.go │ └── error.go ├── go.mod ├── go.sum ├── gorilla ├── methods.go ├── openapi2.go └── openapi3.go ├── internal └── tests │ ├── testfile.go │ ├── testtypes.go │ ├── util.go │ ├── xassert │ └── assert.go │ └── xrequire │ └── require.go ├── openapi2 ├── funcinfo.go ├── funcinfo_test.go ├── funcinfo_zz_test.go ├── log.go ├── marshal.go ├── openapi2.go ├── openapi2_test.go ├── params.go ├── spec.go └── testdata │ ├── t1.json │ └── t2.json └── openapi3 ├── gen.go ├── log.go ├── marshal.go └── openapi3.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.18 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 2 | .vscode 3 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux,go 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,linux,go 5 | 6 | ### Go ### 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | bin/ 18 | 19 | zz.txt 20 | 21 | # Output of the go coverage tool, specifically when used with LiteIDE 22 | *.out 23 | 24 | # Dependency directories (remove the comment below to include it) 25 | # vendor/ 26 | 27 | ### Go Patch ### 28 | /vendor/ 29 | /Godeps/ 30 | 31 | ### Linux ### 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | ### VisualStudioCode ### 47 | .vscode/* 48 | !.vscode/settings.json 49 | !.vscode/tasks.json 50 | !.vscode/launch.json 51 | !.vscode/extensions.json 52 | *.code-workspace 53 | 54 | ### VisualStudioCode Patch ### 55 | # Ignore all local history of files 56 | .history 57 | .ionide 58 | 59 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux,go 60 | 61 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 62 | 63 | posbus 64 | .idea 65 | .favorites.json 66 | .vscode/* 67 | !.vscode/settings.json 68 | !.vscode/tasks.json 69 | !.vscode/launch.json 70 | !.vscode/extensions.json 71 | *.code-workspace 72 | 73 | # Local History for Visual Studio Code 74 | .history/ 75 | 76 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 77 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 78 | 79 | # User-specific stuff 80 | .idea/**/workspace.xml 81 | .idea/**/tasks.xml 82 | .idea/**/usage.statistics.xml 83 | .idea/**/dictionaries 84 | .idea/**/shelf 85 | 86 | # AWS User-specific 87 | .idea/**/aws.xml 88 | 89 | # Generated files 90 | .idea/**/contentModel.xml 91 | 92 | # Sensitive or high-churn files 93 | .idea/**/dataSources/ 94 | .idea/**/dataSources.ids 95 | .idea/**/dataSources.local.xml 96 | .idea/**/sqlDataSources.xml 97 | .idea/**/dynamic.xml 98 | .idea/**/uiDesigner.xml 99 | .idea/**/dbnavigator.xml 100 | 101 | # Gradle 102 | .idea/**/gradle.xml 103 | .idea/**/libraries 104 | 105 | # Gradle and Maven with auto-import 106 | # When using Gradle or Maven with auto-import, you should exclude module files, 107 | # since they will be recreated, and may cause churn. Uncomment if using 108 | # auto-import. 109 | # .idea/artifacts 110 | # .idea/compiler.xml 111 | # .idea/jarRepositories.xml 112 | # .idea/modules.xml 113 | # .idea/*.iml 114 | # *.iml 115 | # *.ipr 116 | .idea/modules.xml 117 | .idea/.gitignore 118 | .idea/vcs.xml 119 | 120 | 121 | # CMake 122 | cmake-build-*/ 123 | 124 | # Mongo Explorer plugin 125 | .idea/**/mongoSettings.xml 126 | 127 | # File-based project format 128 | *.iws 129 | 130 | # IntelliJ 131 | out/ 132 | 133 | # mpeltonen/sbt-idea plugin 134 | .idea_modules/ 135 | 136 | # JIRA plugin 137 | atlassian-ide-plugin.xml 138 | 139 | # Cursive Clojure plugin 140 | .idea/replstate.xml 141 | 142 | # Crashlytics plugin (for Android Studio and IntelliJ) 143 | com_crashlytics_export_strings.xml 144 | crashlytics.properties 145 | crashlytics-build.properties 146 | fabric.properties 147 | 148 | # Editor-based Rest Client 149 | .idea/httpRequests 150 | 151 | # Android studio 3.1+ serialized cache file 152 | .idea/caches/build_file_checksums.ser 153 | dataSources.xml 154 | Project_Default.xml 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Emil Nikolov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chai 2 | 3 | ## Description 4 | 5 | `chai` is an extension for a few popular http routers that adds support for type safe http handlers via Go 1.18's generics. This allows it to also generate a swagger spec by automatically detecting the request/response types, http methods, route paths and path params. 6 | 7 | `chai` uses [swaggo/swag](https://github.com/swaggo/swag) annotations for the parts of the swagger spec that cannot be automatically inferred. 8 | 9 | 10 | ## Supported http routers 11 | 12 | - [chi](https://github.com/go-chi/chi) 13 | - [gorilla/mux](https://github.com/gorilla/mux) 14 | 15 | ## Project status 16 | `chai` is still a work in progress 17 | 18 | ## Gotchas 19 | 20 | - YAML marshalling 21 | 22 | Currently only https://github.com/ghodss/yaml is supported as a yaml marshaller for the generated swagger spec, which is also provided via `openapi2.MarshalYAML()` as an alias 23 | 24 | ## Examples 25 | 26 | - chi - [./examples/chi](./examples/chi) 27 | - gorilla/mux - [./examples/gorilla](./examples/gorilla) 28 | - standalone repo - https://github.com/go-chai/examples 29 | 30 | ## Usage 31 | 32 | This [swagger.yaml](https://editor.swagger.io/?url=https://raw.githubusercontent.com/go-chai/chai/main/examples/docs/basic/swagger.yaml) is generated by the program below. Notice that the spec for the `PostHandler` handler was generated without any annotations. The request/response types and the route were detected automatically from the router itself. 33 | 34 | ![image](https://user-images.githubusercontent.com/1100051/147469383-b257f396-be7c-45d9-bf55-a1f7454bf5bf.png) 35 | 36 | ```go 37 | package main 38 | 39 | import ( 40 | "fmt" 41 | "net/http" 42 | "strconv" 43 | 44 | chai "github.com/go-chai/chai/chi" 45 | _ "github.com/go-chai/chai/examples/docs/basic" // This is required to be able to serve the stored swagger spec in prod 46 | "github.com/go-chai/chai/examples/shared/model" 47 | "github.com/go-chai/chai/openapi2" 48 | "github.com/go-chi/chi/v5" 49 | "github.com/go-openapi/spec" 50 | httpSwagger "github.com/swaggo/http-swagger" 51 | ) 52 | 53 | func main() { 54 | r := chi.NewRouter() 55 | 56 | r.Route("/api/v1", func(r chi.Router) { 57 | r.Route("/examples", func(r chi.Router) { 58 | chai.Post(r, "/post", PostHandler) 59 | chai.Get(r, "/calc", CalcHandler) 60 | chai.Get(r, "/ping", PingHandler) 61 | chai.Get(r, "/groups/{group_id}/accounts/{account_id}", PathParamsHandler) 62 | chai.Get(r, "/header", HeaderHandler) 63 | chai.Get(r, "/securities", SecuritiesHandler) 64 | chai.Get(r, "/attribute", AttributeHandler) 65 | }) 66 | }) 67 | 68 | // This must be used only during development to generate the swagger spec 69 | docs, err := chai.OpenAPI2(r) 70 | if err != nil { 71 | panic(fmt.Sprintf("failed to generate the swagger spec: %+v", err)) 72 | } 73 | 74 | // This should be used in prod to serve the swagger spec 75 | r.Get("/swagger/*", httpSwagger.Handler( 76 | httpSwagger.URL("http://localhost:8080/swagger/doc.json"), //The url pointing to API definition 77 | )) 78 | 79 | addCustomDocs(docs) 80 | 81 | openapi2.LogYAML(docs) 82 | 83 | // This must be used only during development to store the swagger spec 84 | err = openapi2.WriteDocs(docs, &openapi2.GenConfig{ 85 | OutputDir: "examples/docs/basic", 86 | }) 87 | if err != nil { 88 | panic(fmt.Sprintf("failed to write the swagger spec: %+v", err)) 89 | } 90 | 91 | fmt.Println("The swagger spec is available at http://localhost:8080/swagger/") 92 | 93 | http.ListenAndServe(":8080", r) 94 | } 95 | 96 | type Error struct { 97 | Message string `json:"error"` 98 | ErrorDebug string `json:"error_debug,omitempty"` 99 | ErrorDescription string `json:"error_description,omitempty"` 100 | StatusCode int `json:"status_code,omitempty"` 101 | } 102 | 103 | func (e *Error) Error() string { 104 | return e.Message 105 | } 106 | 107 | func PostHandler(account *model.Account, w http.ResponseWriter, r *http.Request) (*model.Account, int, *Error) { 108 | return account, http.StatusOK, nil 109 | } 110 | 111 | // @Param val1 query int true "used for calc" 112 | // @Param val2 query int true "used for calc" 113 | // @Success 203 114 | // @Failure 400,404 115 | func CalcHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 116 | val1, err := strconv.Atoi(r.URL.Query().Get("val1")) 117 | if err != nil { 118 | return "", http.StatusBadRequest, err 119 | } 120 | val2, err := strconv.Atoi(r.URL.Query().Get("val2")) 121 | if err != nil { 122 | return "", http.StatusBadRequest, err 123 | } 124 | return fmt.Sprintf("%d", val1*val2), http.StatusOK, nil 125 | } 126 | 127 | // PingExample godoc 128 | // @Summary ping example 129 | // @Description do ping 130 | // @Tags example 131 | func PingHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 132 | return "pong", http.StatusOK, nil 133 | } 134 | 135 | // PathParamsHandler godoc 136 | // @Summary path params example 137 | // @Description path params 138 | // @Tags example 139 | // @Param group_id path int true "Group ID" 140 | // @Param account_id path int true "Account ID" 141 | // @Failure 400,404 142 | func PathParamsHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 143 | groupID, err := strconv.Atoi(chi.URLParam(r, "group_id")) 144 | if err != nil { 145 | return "", http.StatusBadRequest, err 146 | } 147 | accountID, err := strconv.Atoi(chi.URLParam(r, "account_id")) 148 | if err != nil { 149 | return "", http.StatusBadRequest, err 150 | } 151 | 152 | return fmt.Sprintf("group_id=%d account_id=%d", groupID, accountID), http.StatusOK, nil 153 | } 154 | 155 | // HeaderHandler godoc 156 | // @Summary custome header example 157 | // @Description custome header 158 | // @Tags example 159 | // @Param Authorization header string true "Authentication header" 160 | // @Failure 400,404 161 | func HeaderHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 162 | return r.Header.Get("Authorization"), http.StatusOK, nil 163 | } 164 | 165 | // SecuritiesHandler godoc 166 | // @Summary custome header example 167 | // @Description custome header 168 | // @Tags example 169 | // @Param Authorization header string true "Authentication header" 170 | // @Failure 400,404 171 | // @Security ApiKeyAuth 172 | func SecuritiesHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 173 | return "ok", http.StatusOK, nil 174 | } 175 | 176 | // AttributeHandler godoc 177 | // @Summary attribute example 178 | // @Description attribute 179 | // @Tags example 180 | // @Param enumstring query string false "string enums" Enums(A, B, C) 181 | // @Param enumint query int false "int enums" Enums(1, 2, 3) 182 | // @Param enumnumber query number false "int enums" Enums(1.1, 1.2, 1.3) 183 | // @Param string query string false "string valid" minlength(5) maxlength(10) 184 | // @Param int query int false "int valid" minimum(1) maximum(10) 185 | // @Param default query string false "string default" default(A) 186 | // @Success 200 "answer" 187 | // @Failure 400,404 "ok" 188 | func AttributeHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 189 | return fmt.Sprintf("enumstring=%s enumint=%s enumnumber=%s string=%s int=%s default=%s", 190 | r.URL.Query().Get("enumstring"), 191 | r.URL.Query().Get("enumint"), 192 | r.URL.Query().Get("enumnumber"), 193 | r.URL.Query().Get("string"), 194 | r.URL.Query().Get("int"), 195 | r.URL.Query().Get("default"), 196 | ), http.StatusOK, nil 197 | } 198 | 199 | func addCustomDocs(docs *spec.Swagger) { 200 | docs.Swagger = "2.0" 201 | docs.Host = "localhost:8080" 202 | docs.Info = &spec.Info{ 203 | InfoProps: spec.InfoProps{ 204 | Description: "This is a sample celler server.", 205 | Title: "Swagger Example API", 206 | TermsOfService: "http://swagger.io/terms/", 207 | Contact: &spec.ContactInfo{ 208 | ContactInfoProps: spec.ContactInfoProps{ 209 | Name: "API Support", 210 | URL: "http://www.swagger.io/support", 211 | Email: "support@swagger.io", 212 | }, 213 | }, 214 | License: &spec.License{ 215 | LicenseProps: spec.LicenseProps{ 216 | Name: "Apache 2.0", 217 | URL: "http://www.apache.org/licenses/LICENSE-2.0.html", 218 | }, 219 | }, 220 | Version: "1.0", 221 | }, 222 | } 223 | docs.SecurityDefinitions = map[string]*spec.SecurityScheme{ 224 | "ApiKeyAuth": { 225 | SecuritySchemeProps: spec.SecuritySchemeProps{ 226 | Type: "apiKey", 227 | In: "header", 228 | Name: "Authorization", 229 | }, 230 | }, 231 | } 232 | } 233 | 234 | ``` 235 | -------------------------------------------------------------------------------- /chai.go: -------------------------------------------------------------------------------- 1 | package chai 2 | -------------------------------------------------------------------------------- /chai/chai.go: -------------------------------------------------------------------------------- 1 | package chai 2 | 3 | import ( 4 | "net/http" 5 | 6 | "encoding/json" 7 | ) 8 | 9 | type Methoder interface { 10 | Method(method, pattern string, h http.Handler) 11 | } 12 | 13 | type Reqer interface { 14 | Req() any 15 | } 16 | 17 | type ResErrer interface { 18 | Res() any 19 | Err() any 20 | } 21 | 22 | type Handlerer interface { 23 | Handler() any 24 | } 25 | 26 | func write(w http.ResponseWriter, code int, v any) { 27 | w.Header().Set("Content-Type", "application/json") 28 | w.WriteHeader(code) 29 | json.NewEncoder(w).Encode(v) 30 | } 31 | 32 | func writeErr(w http.ResponseWriter, code int, e ErrType) { 33 | DefaultErrorWriter.WriteError(w, code, e) 34 | } 35 | 36 | func writeBytes(w http.ResponseWriter, code int, bytes []byte) { 37 | w.Header().Set("Content-Type", "application/json") 38 | w.WriteHeader(code) 39 | w.Write(bytes) 40 | } 41 | 42 | type ErrType = error 43 | 44 | type Err error 45 | 46 | type ErrWrap struct { 47 | Err error 48 | StatusCode int 49 | Error string 50 | } 51 | 52 | // TODO figure out how to do this without multiple json.Marshal/Unmarshal calls 53 | func (ew *ErrWrap) MarshalJSON() ([]byte, error) { 54 | m := map[string]interface{}{ 55 | "error": ew.Error, 56 | "status_code": ew.StatusCode, 57 | } 58 | 59 | b, err := json.Marshal(ew.Err) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | err = json.Unmarshal(b, &m) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return json.Marshal(m) 70 | } 71 | 72 | type ErrorWriter interface { 73 | WriteError(w http.ResponseWriter, code int, e ErrType) 74 | } 75 | 76 | type defaultErrorWriter struct{} 77 | 78 | func (defaultErrorWriter) WriteError(w http.ResponseWriter, code int, e ErrType) { 79 | ew := &ErrWrap{ 80 | Err: e, 81 | StatusCode: code, 82 | Error: e.Error(), 83 | } 84 | 85 | b, err := json.Marshal(ew) 86 | if err != nil { 87 | panic(err) 88 | } 89 | 90 | writeBytes(w, code, b) 91 | } 92 | 93 | var DefaultErrorWriter = &defaultErrorWriter{} 94 | -------------------------------------------------------------------------------- /chai/chai_test.go: -------------------------------------------------------------------------------- 1 | package chai_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | 13 | "github.com/go-chai/chai/chai" 14 | "github.com/go-chai/chai/internal/tests" 15 | "github.com/go-chai/chai/internal/tests/xrequire" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func newRes() *tests.TestResponse { 20 | return &tests.TestResponse{ 21 | Foo: "f", 22 | Bar: "b", 23 | TestInnerResponse: tests.TestInnerResponse{ 24 | FooFoo: 123, 25 | BarBar: 12, 26 | }, 27 | } 28 | } 29 | 30 | func newReq() io.Reader { 31 | buf := new(bytes.Buffer) 32 | 33 | json.NewEncoder(buf).Encode(&tests.TestRequest{ 34 | Foo: "312", 35 | Bar: "31321", 36 | TestInnerResponse: tests.TestInnerResponse{ 37 | FooFoo: 4432, 38 | BarBar: 321, 39 | }, 40 | }) 41 | 42 | return buf 43 | } 44 | 45 | func TestHandlers(t *testing.T) { 46 | tests := []struct { 47 | name string 48 | makeHandler func(t *testing.T) http.Handler 49 | response string 50 | }{ 51 | { 52 | name: "req res handler", 53 | makeHandler: func(t *testing.T) http.Handler { 54 | return chai.NewReqResHandler(func(req *tests.TestRequest, w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, error) { 55 | return newRes(), http.StatusOK, nil 56 | }) 57 | }, 58 | response: `{"foo":"f","bar":"b","test_inner_response":{"foo_foo":123,"bar_bar":12}}`, 59 | }, 60 | { 61 | name: "req res handler with error", 62 | makeHandler: func(t *testing.T) http.Handler { 63 | return chai.NewReqResHandler(func(req *tests.TestRequest, w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, error) { 64 | return nil, http.StatusInternalServerError, errors.New("zz") 65 | }) 66 | }, 67 | response: `{"error":"zz", "status_code":500}`, 68 | }, 69 | { 70 | name: "req res handler with custom struct error type with a pointer receiver with no error", 71 | makeHandler: func(t *testing.T) http.Handler { 72 | return chai.NewReqResHandler(func(req *tests.TestRequest, w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, *tests.TestErrorPtr) { 73 | return newRes(), http.StatusOK, nil 74 | }) 75 | }, 76 | response: `{"foo":"f","bar":"b","test_inner_response":{"foo_foo":123,"bar_bar":12}}`, 77 | }, 78 | { 79 | name: "req res handler with custom struct error type with a pointer receiver with error", 80 | makeHandler: func(t *testing.T) http.Handler { 81 | return chai.NewReqResHandler(func(req *tests.TestRequest, w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, *tests.TestErrorPtr) { 82 | return nil, http.StatusInternalServerError, &tests.TestErrorPtr{Message: "zz"} 83 | }) 84 | }, 85 | response: `{"error":"zz", "message":"zz", "status_code":500}`, 86 | }, 87 | { 88 | name: "req res handler with custom struct error type with no error", 89 | makeHandler: func(t *testing.T) http.Handler { 90 | return chai.NewReqResHandler(func(req *tests.TestRequest, w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, tests.TestError) { 91 | return newRes(), http.StatusOK, tests.TestError{} 92 | }) 93 | }, 94 | response: `{"foo":"f","bar":"b","test_inner_response":{"foo_foo":123,"bar_bar":12}}`, 95 | }, 96 | { 97 | name: "req res handler with custom struct error type with error", 98 | makeHandler: func(t *testing.T) http.Handler { 99 | return chai.NewReqResHandler(func(req *tests.TestRequest, w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, tests.TestError) { 100 | return nil, http.StatusInternalServerError, tests.TestError{Message: "zz"} 101 | }) 102 | }, 103 | response: `{"error":"zz", "message":"zz", "status_code":500}`, 104 | }, 105 | { 106 | name: "req res handler with custom map error type with no error", 107 | makeHandler: func(t *testing.T) http.Handler { 108 | return chai.NewReqResHandler(func(req *tests.TestRequest, w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, tests.TestErrorMap) { 109 | return newRes(), http.StatusOK, nil 110 | }) 111 | }, 112 | response: `{"foo":"f","bar":"b","test_inner_response":{"foo_foo":123,"bar_bar":12}}`, 113 | }, 114 | { 115 | name: "req res handler with custom map error type with error", 116 | makeHandler: func(t *testing.T) http.Handler { 117 | return chai.NewReqResHandler(func(req *tests.TestRequest, w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, tests.TestErrorMap) { 118 | return nil, http.StatusInternalServerError, tests.TestErrorMap{"message": "zz"} 119 | }) 120 | }, 121 | response: `{"error":"test error map", "message":"zz", "status_code":500}`, 122 | }, 123 | { 124 | name: "req res handler with custom map error type with a pointer receiver with no error", 125 | makeHandler: func(t *testing.T) http.Handler { 126 | return chai.NewReqResHandler(func(req *tests.TestRequest, w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, *tests.TestErrorMapPtr) { 127 | return newRes(), http.StatusOK, nil 128 | }) 129 | }, 130 | response: `{"foo":"f","bar":"b","test_inner_response":{"foo_foo":123,"bar_bar":12}}`, 131 | }, 132 | { 133 | name: "req res handler with custom map error type with a pointer receiver with error", 134 | makeHandler: func(t *testing.T) http.Handler { 135 | return chai.NewReqResHandler(func(req *tests.TestRequest, w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, *tests.TestErrorMapPtr) { 136 | return nil, http.StatusInternalServerError, &tests.TestErrorMapPtr{"message": "zz"} 137 | }) 138 | }, 139 | response: `{"error":"test error map ptr", "message":"zz", "status_code":500}`, 140 | }, 141 | { 142 | name: "res handler", 143 | makeHandler: func(t *testing.T) http.Handler { 144 | return chai.NewResHandler(func(w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, error) { 145 | return newRes(), http.StatusOK, nil 146 | }) 147 | }, 148 | response: `{"foo":"f","bar":"b","test_inner_response":{"foo_foo":123,"bar_bar":12}}`, 149 | }, 150 | { 151 | name: "res handler with error", 152 | makeHandler: func(t *testing.T) http.Handler { 153 | return chai.NewResHandler(func(w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, error) { 154 | return nil, http.StatusInternalServerError, errors.New("zz") 155 | }) 156 | }, 157 | response: `{"error":"zz", "status_code":500}`, 158 | }, 159 | { 160 | name: "res handler with custom struct error type with a pointer receiver with no error", 161 | makeHandler: func(t *testing.T) http.Handler { 162 | return chai.NewResHandler(func(w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, *tests.TestErrorPtr) { 163 | return newRes(), http.StatusOK, nil 164 | }) 165 | }, 166 | response: `{"foo":"f","bar":"b","test_inner_response":{"foo_foo":123,"bar_bar":12}}`, 167 | }, 168 | { 169 | name: "res handler with custom struct error type with a pointer receiver with error", 170 | makeHandler: func(t *testing.T) http.Handler { 171 | return chai.NewResHandler(func(w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, *tests.TestErrorPtr) { 172 | return nil, http.StatusInternalServerError, &tests.TestErrorPtr{Message: "zz"} 173 | }) 174 | }, 175 | response: `{"error":"zz", "message":"zz", "status_code":500}`, 176 | }, 177 | 178 | { 179 | name: "res handler with custom struct error type with no error", 180 | makeHandler: func(t *testing.T) http.Handler { 181 | return chai.NewResHandler(func(w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, tests.TestError) { 182 | return newRes(), http.StatusOK, tests.TestError{} 183 | }) 184 | }, 185 | response: `{"foo":"f","bar":"b","test_inner_response":{"foo_foo":123,"bar_bar":12}}`, 186 | }, 187 | { 188 | name: "res handler with custom struct error type with error", 189 | makeHandler: func(t *testing.T) http.Handler { 190 | return chai.NewResHandler(func(w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, tests.TestError) { 191 | return nil, http.StatusInternalServerError, tests.TestError{Message: "zz"} 192 | }) 193 | }, 194 | response: `{"error":"zz", "message":"zz", "status_code":500}`, 195 | }, 196 | { 197 | name: "res handler with custom map error type with no error", 198 | makeHandler: func(t *testing.T) http.Handler { 199 | return chai.NewResHandler(func(w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, tests.TestErrorMap) { 200 | return newRes(), http.StatusOK, nil 201 | }) 202 | }, 203 | response: `{"foo":"f","bar":"b","test_inner_response":{"foo_foo":123,"bar_bar":12}}`, 204 | }, 205 | { 206 | name: "res handler with custom map error type with error", 207 | makeHandler: func(t *testing.T) http.Handler { 208 | return chai.NewResHandler(func(w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, tests.TestErrorMap) { 209 | return nil, http.StatusInternalServerError, tests.TestErrorMap{"message": "zz"} 210 | }) 211 | }, 212 | response: `{"error":"test error map", "message":"zz", "status_code":500}`, 213 | }, 214 | { 215 | name: "res handler with custom map error type with a pointer receiver with no error", 216 | makeHandler: func(t *testing.T) http.Handler { 217 | return chai.NewResHandler(func(w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, *tests.TestErrorMapPtr) { 218 | return newRes(), http.StatusOK, nil 219 | }) 220 | }, 221 | response: `{"foo":"f","bar":"b","test_inner_response":{"foo_foo":123,"bar_bar":12}}`, 222 | }, 223 | { 224 | name: "res handler with custom map error type with a pointer receiver with error", 225 | makeHandler: func(t *testing.T) http.Handler { 226 | return chai.NewResHandler(func(w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, *tests.TestErrorMapPtr) { 227 | return nil, http.StatusInternalServerError, &tests.TestErrorMapPtr{"message": "zz"} 228 | }) 229 | }, 230 | response: `{"error":"test error map ptr", "message":"zz", "status_code":500}`, 231 | }, 232 | } 233 | 234 | for _, tt := range tests { 235 | t.Run(tt.name, func(t *testing.T) { 236 | require.NotPanics(t, func() { 237 | h := tt.makeHandler(t) 238 | 239 | w := httptest.NewRecorder() 240 | 241 | h.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/", newReq())) 242 | 243 | fmt.Printf("%q\n", w.Body.String()) 244 | 245 | xrequire.JSONEq(t, tt.response, w.Body.String()) 246 | }) 247 | }) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /chai/req_res_handler.go: -------------------------------------------------------------------------------- 1 | package chai 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "reflect" 7 | ) 8 | 9 | type ReqResHandlerFunc[Req any, Res any, Err ErrType] func(Req, http.ResponseWriter, *http.Request) (Res, int, Err) 10 | 11 | func NewReqResHandler[Req any, Res any, Err ErrType](h ReqResHandlerFunc[Req, Res, Err]) *ReqResHandler[Req, Res, Err] { 12 | return &ReqResHandler[Req, Res, Err]{ 13 | f: h, 14 | } 15 | } 16 | 17 | type ReqResHandler[Req any, Res any, Err ErrType] struct { 18 | f ReqResHandlerFunc[Req, Res, Err] 19 | req *Req 20 | res *Res 21 | err *Err 22 | } 23 | 24 | func isErr[Err ErrType](err Err) bool { 25 | return !reflect.ValueOf(&err).Elem().IsZero() 26 | } 27 | 28 | func (h *ReqResHandler[Req, Res, Err]) ServeHTTP(w http.ResponseWriter, r *http.Request) { 29 | var req *Req 30 | 31 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 32 | writeErr(w, http.StatusBadRequest, err) 33 | return 34 | } 35 | 36 | res, code, err := h.f(*req, w, r) 37 | if isErr(err) { 38 | if code == 0 { 39 | code = http.StatusInternalServerError 40 | } 41 | 42 | writeErr(w, code, err) 43 | return 44 | } 45 | 46 | if code == 0 { 47 | code = http.StatusOK 48 | } 49 | 50 | write(w, code, res) 51 | } 52 | 53 | func (h *ReqResHandler[Req, Res, Err]) Req() any { 54 | return h.req 55 | } 56 | 57 | func (h *ReqResHandler[Req, Res, Err]) Res() any { 58 | return h.res 59 | } 60 | 61 | func (h *ReqResHandler[Req, Res, Err]) Err() any { 62 | return h.err 63 | } 64 | 65 | func (h *ReqResHandler[Req, Res, Err]) Handler() any { 66 | return h.f 67 | } 68 | -------------------------------------------------------------------------------- /chai/res_handler.go: -------------------------------------------------------------------------------- 1 | package chai 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type ResHandlerFunc[Res any, Err ErrType] func(http.ResponseWriter, *http.Request) (Res, int, Err) 8 | 9 | func NewResHandler[Res any, Err ErrType](h ResHandlerFunc[Res, Err]) *ResHandler[Res, Err] { 10 | return &ResHandler[Res, Err]{ 11 | f: h, 12 | } 13 | } 14 | 15 | type ResHandler[Res any, Err ErrType] struct { 16 | f ResHandlerFunc[Res, Err] 17 | res *Res 18 | err *Err 19 | } 20 | 21 | func (h *ResHandler[Res, Err]) ServeHTTP(w http.ResponseWriter, r *http.Request) { 22 | res, code, err := h.f(w, r) 23 | if isErr(err) { 24 | if code == 0 { 25 | code = http.StatusInternalServerError 26 | } 27 | 28 | writeErr(w, code, err) 29 | return 30 | } 31 | 32 | if code == 0 { 33 | code = http.StatusOK 34 | } 35 | 36 | write(w, code, res) 37 | } 38 | 39 | func (h *ResHandler[Res, Err]) Res() any { 40 | return h.res 41 | } 42 | 43 | func (h *ResHandler[Res, Err]) Err() any { 44 | return h.err 45 | } 46 | 47 | func (h *ResHandler[Res, Err]) Handler() any { 48 | return h.f 49 | } 50 | -------------------------------------------------------------------------------- /chi/methods.go: -------------------------------------------------------------------------------- 1 | package chai 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chai/chai/chai" 7 | ) 8 | 9 | func Get[Res any, Err chai.ErrType](r chai.Methoder, path string, fn chai.ResHandlerFunc[Res, Err]) { 10 | r.Method(http.MethodGet, path, chai.NewResHandler(fn)) 11 | } 12 | 13 | func Connect[Res any, Err chai.ErrType](r chai.Methoder, path string, fn chai.ResHandlerFunc[Res, Err]) { 14 | r.Method(http.MethodConnect, path, chai.NewResHandler(fn)) 15 | } 16 | 17 | func Options[Res any, Err chai.ErrType](r chai.Methoder, path string, fn chai.ResHandlerFunc[Res, Err]) { 18 | r.Method(http.MethodOptions, path, chai.NewResHandler(fn)) 19 | } 20 | 21 | func Post[Req any, Res any, Err chai.ErrType](r chai.Methoder, path string, fn chai.ReqResHandlerFunc[Req, Res, Err]) { 22 | r.Method(http.MethodPost, path, chai.NewReqResHandler(fn)) 23 | } 24 | 25 | func Put[Req any, Res any, Err chai.ErrType](r chai.Methoder, path string, fn chai.ReqResHandlerFunc[Req, Res, Err]) { 26 | r.Method(http.MethodPut, path, chai.NewReqResHandler(fn)) 27 | } 28 | 29 | func Patch[Req any, Res any, Err chai.ErrType](r chai.Methoder, path string, fn chai.ReqResHandlerFunc[Req, Res, Err]) { 30 | r.Method(http.MethodPatch, path, chai.NewReqResHandler(fn)) 31 | } 32 | 33 | func Delete[Req any, Res any, Err chai.ErrType](r chai.Methoder, path string, fn chai.ReqResHandlerFunc[Req, Res, Err]) { 34 | r.Method(http.MethodDelete, path, chai.NewReqResHandler(fn)) 35 | } 36 | -------------------------------------------------------------------------------- /chi/openapi2.go: -------------------------------------------------------------------------------- 1 | package chai 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/go-chai/chai/openapi2" 8 | "github.com/go-chi/chi/v5" 9 | "github.com/go-openapi/spec" 10 | ) 11 | 12 | var integerSchema = spec.SimpleSchema{Type: "integer"} 13 | var numberSchema = spec.SimpleSchema{Type: "number"} 14 | 15 | var RegexPatternSchemas = map[string]spec.SimpleSchema{ 16 | "/^(0|-*[1-9]+[0-9]*)$/": integerSchema, 17 | "^[0-9]+$": integerSchema, 18 | "[+-]?([0-9]*[.])?[0-9]+": numberSchema, 19 | } 20 | 21 | func OpenAPI2(r chi.Routes) (*spec.Swagger, error) { 22 | routes, err := getChiRoutes(r) 23 | 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return openapi2.Docs(routes) 29 | } 30 | 31 | func getChiRoutes(r chi.Routes) ([]*openapi2.Route, error) { 32 | routes := make([]*openapi2.Route, 0) 33 | 34 | err := chi.Walk(r, func(method, path string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { 35 | params, regexlessPath := ParsePathParams(path) 36 | routes = append(routes, &openapi2.Route{ 37 | Method: method, 38 | Path: regexlessPath, 39 | Params: params, 40 | Handler: handler, 41 | }) 42 | 43 | return nil 44 | }) 45 | 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return routes, nil 51 | } 52 | 53 | func ParsePathParams(path string) ([]spec.Parameter, string) { 54 | res := make([]spec.Parameter, 0) 55 | regexlessPath := "" 56 | 57 | for { 58 | param, before, after := nextParam(path) 59 | regexlessPath += before 60 | 61 | if param == nil { 62 | break 63 | } 64 | 65 | regexlessPath += "{" + param.Name + "}" 66 | 67 | res = append(res, *param) 68 | path = after 69 | } 70 | 71 | return res, regexlessPath 72 | } 73 | 74 | func nextParam(pattern string) (param *spec.Parameter, before string, after string) { 75 | before, after, found := strings.Cut(pattern, "{") 76 | if !found { 77 | return nil, before, after 78 | } 79 | 80 | // Read to closing } taking into account opens and closes in curl count (cc) 81 | cc := 1 82 | pe := 0 83 | 84 | for i, c := range after { 85 | if c == '{' { 86 | cc++ 87 | } else if c == '}' { 88 | cc-- 89 | 90 | if cc == 0 { 91 | pe = i 92 | break 93 | } 94 | } 95 | } 96 | 97 | key := after[:pe] 98 | after = after[pe+1:] 99 | 100 | key, rexpat, _ := strings.Cut(key, ":") 101 | 102 | if len(rexpat) > 0 { 103 | if rexpat[0] != '^' { 104 | rexpat = "^" + rexpat 105 | } 106 | if rexpat[len(rexpat)-1] != '$' { 107 | rexpat += "$" 108 | } 109 | } 110 | 111 | schema, ok := RegexPatternSchemas[rexpat] 112 | if !ok { 113 | schema = spec.SimpleSchema{ 114 | Type: "string", 115 | } 116 | } 117 | 118 | return &spec.Parameter{ 119 | CommonValidations: spec.CommonValidations{ 120 | Pattern: rexpat, 121 | }, 122 | ParamProps: spec.ParamProps{ 123 | Name: key, 124 | In: "path", 125 | Required: true, 126 | }, 127 | SimpleSchema: schema, 128 | }, before, after 129 | } 130 | -------------------------------------------------------------------------------- /chi/openapi2_test.go: -------------------------------------------------------------------------------- 1 | package chai_test 2 | 3 | import ( 4 | "testing" 5 | 6 | chai "github.com/go-chai/chai/chi" 7 | "github.com/go-chai/chai/examples/shared/controller" 8 | "github.com/go-chai/chai/internal/tests" 9 | "github.com/go-chi/chi/v5" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestOpenAPI2(t *testing.T) { 14 | type args struct { 15 | r chi.Routes 16 | } 17 | tcs := []struct { 18 | name string 19 | args args 20 | filePath string 21 | wantErr bool 22 | }{ 23 | { 24 | name: "celler", 25 | args: args{ 26 | r: controller.NewController().ChiRoutes(), 27 | }, 28 | filePath: "testdata/celler.json", 29 | }, 30 | } 31 | for _, tt := range tcs { 32 | t.Run(tt.name, func(t *testing.T) { 33 | got, err := chai.OpenAPI2(tt.args.r) 34 | require.NoError(t, err) 35 | require.JSONEq(t, tests.LoadFile(t, tt.filePath), tests.JS(got)) 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /chi/openapi3.go: -------------------------------------------------------------------------------- 1 | package chai 2 | 3 | import ( 4 | kinopenapi3 "github.com/getkin/kin-openapi/openapi3" 5 | "github.com/go-chai/chai/openapi3" 6 | "github.com/go-chi/chi/v5" 7 | ) 8 | 9 | func OpenAPI3(r chi.Routes) (*kinopenapi3.T, error) { 10 | routes, err := getChiRoutes(r) 11 | if err != nil { 12 | return nil, err 13 | } 14 | return openapi3.Docs(routes) 15 | } 16 | -------------------------------------------------------------------------------- /examples/chi/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | chai "github.com/go-chai/chai/chi" 9 | _ "github.com/go-chai/chai/examples/docs/basic" // This is required to be able to serve the stored swagger spec in prod 10 | "github.com/go-chai/chai/examples/shared/model" 11 | "github.com/go-chai/chai/openapi2" 12 | "github.com/go-chi/chi/v5" 13 | "github.com/go-openapi/spec" 14 | httpSwagger "github.com/swaggo/http-swagger" 15 | ) 16 | 17 | func main() { 18 | r := chi.NewRouter() 19 | 20 | r.Route("/api/v1", func(r chi.Router) { 21 | r.Route("/examples", func(r chi.Router) { 22 | chai.Post(r, "/post", PostHandler) 23 | chai.Get(r, "/calc", CalcHandler) 24 | chai.Get(r, "/ping", PingHandler) 25 | chai.Get(r, "/groups/{group_id}/accounts/{account_id}", PathParamsHandler) 26 | chai.Get(r, "/header", HeaderHandler) 27 | chai.Get(r, "/securities", SecuritiesHandler) 28 | chai.Get(r, "/attribute", AttributeHandler) 29 | }) 30 | }) 31 | 32 | // This must be used only during development to generate the swagger spec 33 | docs, err := chai.OpenAPI2(r) 34 | if err != nil { 35 | panic(fmt.Sprintf("failed to generate the swagger spec: %+v", err)) 36 | } 37 | 38 | // This should be used in prod to serve the swagger spec 39 | r.Get("/swagger/*", httpSwagger.Handler( 40 | httpSwagger.URL("http://localhost:8080/swagger/doc.json"), //The url pointing to API definition 41 | )) 42 | 43 | addCustomDocs(docs) 44 | 45 | openapi2.LogYAML(docs) 46 | 47 | // This must be used only during development to store the swagger spec 48 | err = openapi2.WriteDocs(docs, &openapi2.GenConfig{ 49 | OutputDir: "examples/docs/basic", 50 | }) 51 | if err != nil { 52 | panic(fmt.Sprintf("failed to write the swagger spec: %+v", err)) 53 | } 54 | 55 | fmt.Println("The swagger spec is available at http://localhost:8080/swagger/") 56 | 57 | http.ListenAndServe(":8080", r) 58 | } 59 | 60 | type Error struct { 61 | Message string `json:"error"` 62 | ErrorDebug string `json:"error_debug,omitempty"` 63 | ErrorDescription string `json:"error_description,omitempty"` 64 | StatusCode int `json:"status_code,omitempty"` 65 | } 66 | 67 | func (e *Error) Error() string { 68 | return e.Message 69 | } 70 | 71 | func PostHandler(account *model.Account, w http.ResponseWriter, r *http.Request) (*model.Account, int, *Error) { 72 | return account, http.StatusOK, nil 73 | } 74 | 75 | // @Param val1 query int true "used for calc" 76 | // @Param val2 query int true "used for calc" 77 | // @Success 203 78 | // @Failure 400,404 79 | func CalcHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 80 | val1, err := strconv.Atoi(r.URL.Query().Get("val1")) 81 | if err != nil { 82 | return "", http.StatusBadRequest, err 83 | } 84 | val2, err := strconv.Atoi(r.URL.Query().Get("val2")) 85 | if err != nil { 86 | return "", http.StatusBadRequest, err 87 | } 88 | return fmt.Sprintf("%d", val1*val2), http.StatusOK, nil 89 | } 90 | 91 | // PingExample godoc 92 | // @Summary ping example 93 | // @Description do ping 94 | // @Tags example 95 | func PingHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 96 | return "pong", http.StatusOK, nil 97 | } 98 | 99 | // PathParamsHandler godoc 100 | // @Summary path params example 101 | // @Description path params 102 | // @Tags example 103 | // @Param group_id path int true "Group ID" 104 | // @Param account_id path int true "Account ID" 105 | // @Failure 400,404 106 | func PathParamsHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 107 | groupID, err := strconv.Atoi(chi.URLParam(r, "group_id")) 108 | if err != nil { 109 | return "", http.StatusBadRequest, err 110 | } 111 | accountID, err := strconv.Atoi(chi.URLParam(r, "account_id")) 112 | if err != nil { 113 | return "", http.StatusBadRequest, err 114 | } 115 | 116 | return fmt.Sprintf("group_id=%d account_id=%d", groupID, accountID), http.StatusOK, nil 117 | } 118 | 119 | // HeaderHandler godoc 120 | // @Summary custome header example 121 | // @Description custome header 122 | // @Tags example 123 | // @Param Authorization header string true "Authentication header" 124 | // @Failure 400,404 125 | func HeaderHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 126 | return r.Header.Get("Authorization"), http.StatusOK, nil 127 | } 128 | 129 | // SecuritiesHandler godoc 130 | // @Summary custome header example 131 | // @Description custome header 132 | // @Tags example 133 | // @Param Authorization header string true "Authentication header" 134 | // @Failure 400,404 135 | // @Security ApiKeyAuth 136 | func SecuritiesHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 137 | return "ok", http.StatusOK, nil 138 | } 139 | 140 | // AttributeHandler godoc 141 | // @Summary attribute example 142 | // @Description attribute 143 | // @Tags example 144 | // @Param enumstring query string false "string enums" Enums(A, B, C) 145 | // @Param enumint query int false "int enums" Enums(1, 2, 3) 146 | // @Param enumnumber query number false "int enums" Enums(1.1, 1.2, 1.3) 147 | // @Param string query string false "string valid" minlength(5) maxlength(10) 148 | // @Param int query int false "int valid" minimum(1) maximum(10) 149 | // @Param default query string false "string default" default(A) 150 | // @Success 200 "answer" 151 | // @Failure 400,404 "ok" 152 | func AttributeHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 153 | return fmt.Sprintf("enumstring=%s enumint=%s enumnumber=%s string=%s int=%s default=%s", 154 | r.URL.Query().Get("enumstring"), 155 | r.URL.Query().Get("enumint"), 156 | r.URL.Query().Get("enumnumber"), 157 | r.URL.Query().Get("string"), 158 | r.URL.Query().Get("int"), 159 | r.URL.Query().Get("default"), 160 | ), http.StatusOK, nil 161 | } 162 | 163 | func addCustomDocs(docs *spec.Swagger) { 164 | docs.Swagger = "2.0" 165 | docs.Host = "localhost:8080" 166 | docs.Info = &spec.Info{ 167 | InfoProps: spec.InfoProps{ 168 | Description: "This is a sample celler server.", 169 | Title: "Swagger Example API", 170 | TermsOfService: "http://swagger.io/terms/", 171 | Contact: &spec.ContactInfo{ 172 | ContactInfoProps: spec.ContactInfoProps{ 173 | Name: "API Support", 174 | URL: "http://www.swagger.io/support", 175 | Email: "support@swagger.io", 176 | }, 177 | }, 178 | License: &spec.License{ 179 | LicenseProps: spec.LicenseProps{ 180 | Name: "Apache 2.0", 181 | URL: "http://www.apache.org/licenses/LICENSE-2.0.html", 182 | }, 183 | }, 184 | Version: "1.0", 185 | }, 186 | } 187 | docs.SecurityDefinitions = map[string]*spec.SecurityScheme{ 188 | "ApiKeyAuth": { 189 | SecuritySchemeProps: spec.SecuritySchemeProps{ 190 | Type: "apiKey", 191 | In: "header", 192 | Name: "Authorization", 193 | }, 194 | }, 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /examples/chi/celler/README.md: -------------------------------------------------------------------------------- 1 | # Celler example for github.com/go-chai/chai 2 | 3 | Run the app 4 | 5 | ```console 6 | $ go run -gcflags -N ./examples/chi/celler/main.go 7 | ``` 8 | 9 | [open swagger](http://localhost:8080/swagger/index.html) 10 | -------------------------------------------------------------------------------- /examples/chi/celler/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/ghodss/yaml" 9 | 10 | chai "github.com/go-chai/chai/chi" 11 | "github.com/go-chai/chai/examples/shared/controller" 12 | 13 | _ "github.com/go-chai/chai/examples/docs/celler" // This is required to be able to serve the stored swagger spec in prod 14 | "github.com/go-chai/chai/examples/shared/httputil" 15 | "github.com/go-chai/chai/openapi2" 16 | "github.com/go-chi/chi/v5" 17 | "github.com/go-openapi/spec" 18 | httpSwagger "github.com/swaggo/http-swagger" 19 | ) 20 | 21 | func main() { 22 | r := chi.NewRouter() 23 | 24 | c := controller.NewController() 25 | 26 | r.Mount("/", c.ChiRoutes()) 27 | 28 | // This must be used only during development to generate the swagger spec 29 | docs, err := chai.OpenAPI2(r) 30 | if err != nil { 31 | panic(fmt.Sprintf("failed to generate the swagger spec: %+v", err)) 32 | } 33 | 34 | // This should be used in prod to serve the swagger spec 35 | r.Get("/swagger/*", httpSwagger.Handler( 36 | httpSwagger.URL("http://localhost:8080/swagger/doc.json"), //The url pointing to API definition 37 | )) 38 | 39 | addCustomDocs(docs) 40 | 41 | LogYAML(docs) 42 | 43 | // This must be used only during development to store the swagger spec 44 | err = openapi2.WriteDocs(docs, &openapi2.GenConfig{ 45 | OutputDir: "examples/docs/celler", 46 | }) 47 | if err != nil { 48 | panic(fmt.Sprintf("failed to write the swagger spec: %+v", err)) 49 | } 50 | 51 | fmt.Println("The swagger spec is available at http://localhost:8080/swagger/") 52 | 53 | http.ListenAndServe(":8080", r) 54 | } 55 | 56 | func addCustomDocs(docs *spec.Swagger) { 57 | docs.Swagger = "2.0" 58 | docs.Host = "localhost:8080" 59 | docs.Info = &spec.Info{ 60 | InfoProps: spec.InfoProps{ 61 | Description: "This is a sample celler server.", 62 | Title: "Swagger Example API", 63 | TermsOfService: "http://swagger.io/terms/", 64 | Contact: &spec.ContactInfo{ 65 | ContactInfoProps: spec.ContactInfoProps{ 66 | Name: "API Support", 67 | URL: "http://www.swagger.io/support", 68 | Email: "support@swagger.io", 69 | }, 70 | }, 71 | License: &spec.License{ 72 | LicenseProps: spec.LicenseProps{ 73 | Name: "Apache 2.0", 74 | URL: "http://www.apache.org/licenses/LICENSE-2.0.html", 75 | }, 76 | }, 77 | Version: "1.0", 78 | }, 79 | } 80 | docs.SecurityDefinitions = map[string]*spec.SecurityScheme{ 81 | "BasicAuth": { 82 | SecuritySchemeProps: spec.SecuritySchemeProps{ 83 | Type: "basic", 84 | }, 85 | }, 86 | "ApiKeyAuth": { 87 | SecuritySchemeProps: spec.SecuritySchemeProps{ 88 | Type: "apiKey", 89 | In: "header", 90 | Name: "Authorization", 91 | }, 92 | }, 93 | "OAuth2Implicit": { 94 | SecuritySchemeProps: spec.SecuritySchemeProps{ 95 | Description: "Use with the OAuth2 Implicit Grant to retrieve a token", 96 | Type: "oauth2", 97 | Flow: "implicit", 98 | AuthorizationURL: "https://example.com/oauth/authorize", 99 | TokenURL: "", 100 | Scopes: map[string]string{ 101 | "admin": "Grants read and write access to administrative information", 102 | "write": "Grants write access", 103 | }, 104 | }, 105 | }, 106 | "OAuth2Application": { 107 | SecuritySchemeProps: spec.SecuritySchemeProps{ 108 | Description: "Use with the OAuth2 Implicit Grant to retrieve a token", 109 | Type: "oauth2", 110 | Flow: "application", 111 | TokenURL: "https://example.com/oauth/token", 112 | Scopes: map[string]string{ 113 | "admin": "Grants read and write access to administrative information", 114 | "write": "Grants write access", 115 | }, 116 | }, 117 | }, 118 | 119 | "OAuth2Password": { 120 | SecuritySchemeProps: spec.SecuritySchemeProps{ 121 | Type: "oauth2", 122 | Flow: "password", 123 | TokenURL: "https://example.com/oauth/token", 124 | Scopes: map[string]string{ 125 | "admin": "Grants read and write access to administrative information", 126 | "write": "Grants write access", 127 | "read": "Grants read access", 128 | }, 129 | }, 130 | }, 131 | "OAuth2AccessToken": { 132 | SecuritySchemeProps: spec.SecuritySchemeProps{ 133 | Type: "oauth2", 134 | Flow: "accessCode", 135 | AuthorizationURL: "https://example.com/oauth/authorize", 136 | TokenURL: "https://example.com/oauth/token", 137 | Scopes: map[string]string{ 138 | "admin": "Grants read and write access to administrative information", 139 | }, 140 | }, 141 | }, 142 | } 143 | } 144 | 145 | func auth(next http.Handler) http.Handler { 146 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 147 | if len(r.Header.Get("Authorization")) == 0 { 148 | httputil.NewError(w, http.StatusUnauthorized, errors.New("Authorization is required Header")) 149 | return 150 | } 151 | next.ServeHTTP(w, r) 152 | }) 153 | } 154 | 155 | func LogYAML(v any) { 156 | bytes, err := yaml.Marshal(v) 157 | if err != nil { 158 | panic(err) 159 | } 160 | 161 | fmt.Println(string(bytes)) 162 | 163 | return 164 | } 165 | -------------------------------------------------------------------------------- /examples/chi/openapi3/README.md: -------------------------------------------------------------------------------- 1 | # Celler example for github.com/go-chai/chai 2 | 3 | Run the app 4 | 5 | ```console 6 | $ go run -gcflags -N ./examples/chi/openapi3/main.go 7 | ``` 8 | 9 | [open swagger](http://localhost:8080/swagger/index.html) 10 | -------------------------------------------------------------------------------- /examples/chi/openapi3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/ghodss/yaml" 9 | 10 | chai "github.com/go-chai/chai/chi" 11 | "github.com/go-chai/chai/examples/shared/controller" 12 | 13 | kinopenapi3 "github.com/getkin/kin-openapi/openapi3" 14 | 15 | _ "github.com/go-chai/chai/examples/docs/openapi3" // This is required to be able to serve the stored swagger spec in prod 16 | "github.com/go-chai/chai/examples/shared/httputil" 17 | "github.com/go-chai/chai/openapi3" 18 | "github.com/go-chi/chi/v5" 19 | "github.com/go-chi/chi/v5/middleware" 20 | httpSwagger "github.com/swaggo/http-swagger" 21 | ) 22 | 23 | func main() { 24 | r := chi.NewRouter() 25 | 26 | r.Use(middleware.Logger) 27 | 28 | c := controller.NewController() 29 | 30 | r.Route("/api/v1", func(r chi.Router) { 31 | r.Route("/accounts", func(r chi.Router) { 32 | chai.Get(r, "/{id}", c.ShowAccount) 33 | chai.Get(r, "/", c.ListAccounts) 34 | chai.Post(r, "/", c.AddAccount) 35 | r.Delete("/{id:[0-9]+}", c.DeleteAccount) 36 | r.Patch("/{id}", c.UpdateAccount) 37 | r.Post("/{id}/images", c.UploadAccountImage) 38 | }) 39 | 40 | r.Route("/bottles", func(r chi.Router) { 41 | chai.Get(r, "/{id}", c.ShowBottle) 42 | chai.Get(r, "/", c.ListBottles) 43 | }) 44 | 45 | r.Route("/admin", func(r chi.Router) { 46 | r.Use(auth) 47 | 48 | chai.Post(r, "/auth", c.Auth) 49 | }) 50 | 51 | r.Route("/examples", func(r chi.Router) { 52 | chai.Get(r, "/ping", c.PingExample) 53 | chai.Get(r, "/calc", c.CalcExample) 54 | // chai.Get(r, "/group{s/{gro}up_id}/accounts/{account_id}", c.PathParamsExample) 55 | chai.Get(r, "/groups/{group_id}/accounts/{account_id}", c.PathParamsExample) 56 | chai.Get(r, "/header", c.HeaderExample) 57 | chai.Get(r, "/securities", c.SecuritiesExample) 58 | chai.Get(r, "/attribute", c.AttributeExample) 59 | chai.Post(r, "/attribute", c.PostExample) 60 | }) 61 | }) 62 | 63 | // This must be used only during development to generate the swagger spec 64 | docs, err := chai.OpenAPI3(r) 65 | if err != nil { 66 | panic(fmt.Sprintf("failed to generate the swagger spec: %+v", err)) 67 | } 68 | 69 | // This should be used in prod to serve the swagger spec 70 | r.Get("/swagger/*", httpSwagger.Handler( 71 | httpSwagger.URL("http://localhost:8080/swagger/doc.json"), //The url pointing to API definition 72 | )) 73 | 74 | addCustomDocs(docs) 75 | 76 | LogYAML(docs) 77 | 78 | // This must be used only during development to store the swagger spec 79 | err = openapi3.WriteDocs(docs, &openapi3.GenConfig{ 80 | OutputDir: "examples/docs/openapi3", 81 | }) 82 | if err != nil { 83 | panic(fmt.Sprintf("failed to write the swagger spec: %+v", err)) 84 | } 85 | 86 | fmt.Println("The swagger spec is available at http://localhost:8080/swagger/") 87 | 88 | http.ListenAndServe(":8080", r) 89 | } 90 | 91 | func addCustomDocs(docs *kinopenapi3.T) { 92 | docs.Servers = []*kinopenapi3.Server{ 93 | { 94 | URL: "localhost:8080", 95 | }, 96 | } 97 | docs.Info = &kinopenapi3.Info{ 98 | ExtensionProps: kinopenapi3.ExtensionProps{}, 99 | Description: "This is a sample celler server.", 100 | Title: "Swagger Example API", 101 | TermsOfService: "http://swagger.io/terms/", 102 | Contact: &kinopenapi3.Contact{ 103 | Name: "API Support", 104 | URL: "http://www.swagger.io/support", 105 | Email: "support@swagger.io", 106 | }, 107 | License: &kinopenapi3.License{ 108 | Name: "Apache 2.0", 109 | URL: "http://www.apache.org/licenses/LICENSE-2.0.html", 110 | }, 111 | Version: "1.0", 112 | } 113 | 114 | docs.Components.SecuritySchemes = kinopenapi3.SecuritySchemes{ 115 | "BasicAuth": { 116 | Value: kinopenapi3.NewJWTSecurityScheme(), 117 | }, 118 | "ApiKeyAuth": { 119 | Value: kinopenapi3.NewCSRFSecurityScheme(), 120 | }, 121 | "OAuth2Implicit": { 122 | Value: kinopenapi3.NewCSRFSecurityScheme(), 123 | }, 124 | "OAuth2Application": { 125 | Value: kinopenapi3.NewCSRFSecurityScheme(), 126 | }, 127 | "OAuth2Password": { 128 | Value: kinopenapi3.NewCSRFSecurityScheme(), 129 | }, 130 | "OAuth2AccessToken": { 131 | Value: kinopenapi3.NewCSRFSecurityScheme(), 132 | }, 133 | } 134 | } 135 | 136 | func auth(next http.Handler) http.Handler { 137 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 138 | if len(r.Header.Get("Authorization")) == 0 { 139 | httputil.NewError(w, http.StatusUnauthorized, errors.New("Authorization is required Header")) 140 | return 141 | } 142 | next.ServeHTTP(w, r) 143 | }) 144 | } 145 | 146 | func LogYAML(v any) { 147 | bytes, err := yaml.Marshal(v) 148 | if err != nil { 149 | panic(err) 150 | } 151 | 152 | fmt.Println(string(bytes)) 153 | 154 | return 155 | } 156 | -------------------------------------------------------------------------------- /examples/docs/basic/docs.go: -------------------------------------------------------------------------------- 1 | // Package basic GENERATED BY THE COMMAND ABOVE; DO NOT EDIT 2 | // This file was generated by swaggo/swag 3 | package basic 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "strings" 9 | "text/template" 10 | 11 | "github.com/swaggo/swag" 12 | ) 13 | 14 | var doc = `{ 15 | "schemes": {{ marshal .Schemes }}, 16 | "swagger": "2.0", 17 | "info": { 18 | "description": "{{escape .Description}}", 19 | "title": "{{.Title}}", 20 | "termsOfService": "http://swagger.io/terms/", 21 | "contact": { 22 | "name": "API Support", 23 | "url": "http://www.swagger.io/support", 24 | "email": "support@swagger.io" 25 | }, 26 | "license": { 27 | "name": "Apache 2.0", 28 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 29 | }, 30 | "version": "{{.Version}}" 31 | }, 32 | "host": "{{.Host}}", 33 | "basePath": "{{.BasePath}}", 34 | "paths": { 35 | "/api/v1/examples/attribute": { 36 | "get": { 37 | "description": "attribute", 38 | "produces": [ 39 | "application/json" 40 | ], 41 | "tags": [ 42 | "example" 43 | ], 44 | "summary": "attribute example", 45 | "parameters": [ 46 | { 47 | "type": "string", 48 | "default": "A", 49 | "description": "string default", 50 | "name": "default", 51 | "in": "query" 52 | }, 53 | { 54 | "enum": [ 55 | 1, 56 | 2, 57 | 3 58 | ], 59 | "type": "integer", 60 | "description": "int enums", 61 | "name": "enumint", 62 | "in": "query" 63 | }, 64 | { 65 | "enum": [ 66 | 1.1, 67 | 1.2, 68 | 1.3 69 | ], 70 | "type": "number", 71 | "description": "int enums", 72 | "name": "enumnumber", 73 | "in": "query" 74 | }, 75 | { 76 | "enum": [ 77 | "A", 78 | "B", 79 | "C" 80 | ], 81 | "type": "string", 82 | "description": "string enums", 83 | "name": "enumstring", 84 | "in": "query" 85 | }, 86 | { 87 | "maximum": 10, 88 | "minimum": 1, 89 | "type": "integer", 90 | "description": "int valid", 91 | "name": "int", 92 | "in": "query" 93 | }, 94 | { 95 | "maxLength": 10, 96 | "minLength": 5, 97 | "type": "string", 98 | "description": "string valid", 99 | "name": "string", 100 | "in": "query" 101 | } 102 | ], 103 | "responses": { 104 | "200": { 105 | "description": "answer", 106 | "schema": { 107 | "type": "string" 108 | } 109 | }, 110 | "400": { 111 | "description": "ok", 112 | "schema": { 113 | "type": "string" 114 | } 115 | }, 116 | "404": { 117 | "description": "ok", 118 | "schema": { 119 | "type": "string" 120 | } 121 | } 122 | } 123 | } 124 | }, 125 | "/api/v1/examples/calc": { 126 | "get": { 127 | "produces": [ 128 | "application/json" 129 | ], 130 | "parameters": [ 131 | { 132 | "type": "integer", 133 | "description": "used for calc", 134 | "name": "val1", 135 | "in": "query", 136 | "required": true 137 | }, 138 | { 139 | "type": "integer", 140 | "description": "used for calc", 141 | "name": "val2", 142 | "in": "query", 143 | "required": true 144 | } 145 | ], 146 | "responses": { 147 | "203": { 148 | "description": "", 149 | "schema": { 150 | "type": "string" 151 | } 152 | }, 153 | "400": { 154 | "description": "", 155 | "schema": { 156 | "type": "string" 157 | } 158 | }, 159 | "404": { 160 | "description": "", 161 | "schema": { 162 | "type": "string" 163 | } 164 | } 165 | } 166 | } 167 | }, 168 | "/api/v1/examples/groups/{group_id}/accounts/{account_id}": { 169 | "get": { 170 | "description": "path params", 171 | "produces": [ 172 | "application/json" 173 | ], 174 | "tags": [ 175 | "example" 176 | ], 177 | "summary": "path params example", 178 | "parameters": [ 179 | { 180 | "type": "integer", 181 | "description": "Account ID", 182 | "name": "account_id", 183 | "in": "path", 184 | "required": true 185 | }, 186 | { 187 | "type": "integer", 188 | "description": "Group ID", 189 | "name": "group_id", 190 | "in": "path", 191 | "required": true 192 | } 193 | ], 194 | "responses": { 195 | "200": { 196 | "description": "", 197 | "schema": { 198 | "type": "string" 199 | } 200 | }, 201 | "400": { 202 | "description": "", 203 | "schema": { 204 | "type": "string" 205 | } 206 | }, 207 | "404": { 208 | "description": "", 209 | "schema": { 210 | "type": "string" 211 | } 212 | } 213 | } 214 | } 215 | }, 216 | "/api/v1/examples/header": { 217 | "get": { 218 | "description": "custome header", 219 | "produces": [ 220 | "application/json" 221 | ], 222 | "tags": [ 223 | "example" 224 | ], 225 | "summary": "custome header example", 226 | "parameters": [ 227 | { 228 | "type": "string", 229 | "description": "Authentication header", 230 | "name": "Authorization", 231 | "in": "header", 232 | "required": true 233 | } 234 | ], 235 | "responses": { 236 | "200": { 237 | "description": "", 238 | "schema": { 239 | "type": "string" 240 | } 241 | }, 242 | "400": { 243 | "description": "", 244 | "schema": { 245 | "type": "string" 246 | } 247 | }, 248 | "404": { 249 | "description": "", 250 | "schema": { 251 | "type": "string" 252 | } 253 | } 254 | } 255 | } 256 | }, 257 | "/api/v1/examples/ping": { 258 | "get": { 259 | "description": "do ping", 260 | "produces": [ 261 | "application/json" 262 | ], 263 | "tags": [ 264 | "example" 265 | ], 266 | "summary": "ping example", 267 | "responses": { 268 | "200": { 269 | "description": "", 270 | "schema": { 271 | "type": "string" 272 | } 273 | }, 274 | "default": { 275 | "description": "", 276 | "schema": { 277 | "type": "string" 278 | } 279 | } 280 | } 281 | } 282 | }, 283 | "/api/v1/examples/post": { 284 | "post": { 285 | "consumes": [ 286 | "application/json" 287 | ], 288 | "produces": [ 289 | "application/json" 290 | ], 291 | "parameters": [ 292 | { 293 | "name": "body", 294 | "in": "body", 295 | "schema": { 296 | "$ref": "#/definitions/model.Account" 297 | } 298 | } 299 | ], 300 | "responses": { 301 | "200": { 302 | "description": "", 303 | "schema": { 304 | "$ref": "#/definitions/model.Account" 305 | } 306 | }, 307 | "default": { 308 | "description": "", 309 | "schema": { 310 | "$ref": "#/definitions/main.Error" 311 | } 312 | } 313 | } 314 | } 315 | }, 316 | "/api/v1/examples/securities": { 317 | "get": { 318 | "security": [ 319 | { 320 | "ApiKeyAuth": [] 321 | } 322 | ], 323 | "description": "custome header", 324 | "produces": [ 325 | "application/json" 326 | ], 327 | "tags": [ 328 | "example" 329 | ], 330 | "summary": "custome header example", 331 | "parameters": [ 332 | { 333 | "type": "string", 334 | "description": "Authentication header", 335 | "name": "Authorization", 336 | "in": "header", 337 | "required": true 338 | } 339 | ], 340 | "responses": { 341 | "200": { 342 | "description": "", 343 | "schema": { 344 | "type": "string" 345 | } 346 | }, 347 | "400": { 348 | "description": "", 349 | "schema": { 350 | "type": "string" 351 | } 352 | }, 353 | "404": { 354 | "description": "", 355 | "schema": { 356 | "type": "string" 357 | } 358 | } 359 | } 360 | } 361 | } 362 | }, 363 | "definitions": { 364 | "main.Error": { 365 | "type": "object", 366 | "properties": { 367 | "error": { 368 | "type": "string" 369 | }, 370 | "error_debug": { 371 | "type": "string" 372 | }, 373 | "error_description": { 374 | "type": "string" 375 | }, 376 | "status_code": { 377 | "type": "integer" 378 | } 379 | } 380 | }, 381 | "model.Account": { 382 | "type": "object", 383 | "properties": { 384 | "id": { 385 | "type": "integer", 386 | "format": "int64", 387 | "example": 1 388 | }, 389 | "name": { 390 | "type": "string", 391 | "example": "account name" 392 | }, 393 | "some_number": { 394 | "type": "integer", 395 | "example": 1234 396 | }, 397 | "uuid": { 398 | "type": "string", 399 | "format": "uuid", 400 | "example": "550e8400-e29b-41d4-a716-446655440000" 401 | } 402 | } 403 | } 404 | }, 405 | "securityDefinitions": { 406 | "ApiKeyAuth": { 407 | "type": "apiKey", 408 | "name": "Authorization", 409 | "in": "header" 410 | } 411 | } 412 | }` 413 | 414 | type swaggerInfo struct { 415 | Version string 416 | Host string 417 | BasePath string 418 | Schemes []string 419 | Title string 420 | Description string 421 | } 422 | 423 | // SwaggerInfo holds exported Swagger Info so clients can modify it 424 | var SwaggerInfo = swaggerInfo{ 425 | Version: "1.0", 426 | Host: "localhost:8080", 427 | BasePath: "", 428 | Schemes: []string{}, 429 | Title: "Swagger Example API", 430 | Description: "This is a sample celler server.", 431 | } 432 | 433 | type s struct{} 434 | 435 | func (s *s) ReadDoc() string { 436 | sInfo := SwaggerInfo 437 | sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) 438 | 439 | t, err := template.New("swagger_info").Funcs(template.FuncMap{ 440 | "marshal": func(v interface{}) string { 441 | a, _ := json.Marshal(v) 442 | return string(a) 443 | }, 444 | "escape": func(v interface{}) string { 445 | // escape tabs 446 | str := strings.Replace(v.(string), "\t", "\\t", -1) 447 | // replace " with \", and if that results in \\", replace that with \\\" 448 | str = strings.Replace(str, "\"", "\\\"", -1) 449 | return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1) 450 | }, 451 | }).Parse(doc) 452 | if err != nil { 453 | return doc 454 | } 455 | 456 | var tpl bytes.Buffer 457 | if err := t.Execute(&tpl, sInfo); err != nil { 458 | return doc 459 | } 460 | 461 | return tpl.String() 462 | } 463 | 464 | func init() { 465 | swag.Register("swagger", &s{}) 466 | } 467 | -------------------------------------------------------------------------------- /examples/docs/basic/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "This is a sample celler server.", 5 | "title": "Swagger Example API", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": { 8 | "name": "API Support", 9 | "url": "http://www.swagger.io/support", 10 | "email": "support@swagger.io" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | }, 16 | "version": "1.0" 17 | }, 18 | "host": "localhost:8080", 19 | "paths": { 20 | "/api/v1/examples/attribute": { 21 | "get": { 22 | "description": "attribute", 23 | "produces": [ 24 | "application/json" 25 | ], 26 | "tags": [ 27 | "example" 28 | ], 29 | "summary": "attribute example", 30 | "parameters": [ 31 | { 32 | "type": "string", 33 | "default": "A", 34 | "description": "string default", 35 | "name": "default", 36 | "in": "query" 37 | }, 38 | { 39 | "enum": [ 40 | 1, 41 | 2, 42 | 3 43 | ], 44 | "type": "integer", 45 | "description": "int enums", 46 | "name": "enumint", 47 | "in": "query" 48 | }, 49 | { 50 | "enum": [ 51 | 1.1, 52 | 1.2, 53 | 1.3 54 | ], 55 | "type": "number", 56 | "description": "int enums", 57 | "name": "enumnumber", 58 | "in": "query" 59 | }, 60 | { 61 | "enum": [ 62 | "A", 63 | "B", 64 | "C" 65 | ], 66 | "type": "string", 67 | "description": "string enums", 68 | "name": "enumstring", 69 | "in": "query" 70 | }, 71 | { 72 | "maximum": 10, 73 | "minimum": 1, 74 | "type": "integer", 75 | "description": "int valid", 76 | "name": "int", 77 | "in": "query" 78 | }, 79 | { 80 | "maxLength": 10, 81 | "minLength": 5, 82 | "type": "string", 83 | "description": "string valid", 84 | "name": "string", 85 | "in": "query" 86 | } 87 | ], 88 | "responses": { 89 | "200": { 90 | "description": "answer", 91 | "schema": { 92 | "type": "string" 93 | } 94 | }, 95 | "400": { 96 | "description": "ok", 97 | "schema": { 98 | "type": "string" 99 | } 100 | }, 101 | "404": { 102 | "description": "ok", 103 | "schema": { 104 | "type": "string" 105 | } 106 | } 107 | } 108 | } 109 | }, 110 | "/api/v1/examples/calc": { 111 | "get": { 112 | "produces": [ 113 | "application/json" 114 | ], 115 | "parameters": [ 116 | { 117 | "type": "integer", 118 | "description": "used for calc", 119 | "name": "val1", 120 | "in": "query", 121 | "required": true 122 | }, 123 | { 124 | "type": "integer", 125 | "description": "used for calc", 126 | "name": "val2", 127 | "in": "query", 128 | "required": true 129 | } 130 | ], 131 | "responses": { 132 | "203": { 133 | "description": "", 134 | "schema": { 135 | "type": "string" 136 | } 137 | }, 138 | "400": { 139 | "description": "", 140 | "schema": { 141 | "type": "string" 142 | } 143 | }, 144 | "404": { 145 | "description": "", 146 | "schema": { 147 | "type": "string" 148 | } 149 | } 150 | } 151 | } 152 | }, 153 | "/api/v1/examples/groups/{group_id}/accounts/{account_id}": { 154 | "get": { 155 | "description": "path params", 156 | "produces": [ 157 | "application/json" 158 | ], 159 | "tags": [ 160 | "example" 161 | ], 162 | "summary": "path params example", 163 | "parameters": [ 164 | { 165 | "type": "integer", 166 | "description": "Account ID", 167 | "name": "account_id", 168 | "in": "path", 169 | "required": true 170 | }, 171 | { 172 | "type": "integer", 173 | "description": "Group ID", 174 | "name": "group_id", 175 | "in": "path", 176 | "required": true 177 | } 178 | ], 179 | "responses": { 180 | "200": { 181 | "description": "", 182 | "schema": { 183 | "type": "string" 184 | } 185 | }, 186 | "400": { 187 | "description": "", 188 | "schema": { 189 | "type": "string" 190 | } 191 | }, 192 | "404": { 193 | "description": "", 194 | "schema": { 195 | "type": "string" 196 | } 197 | } 198 | } 199 | } 200 | }, 201 | "/api/v1/examples/header": { 202 | "get": { 203 | "description": "custome header", 204 | "produces": [ 205 | "application/json" 206 | ], 207 | "tags": [ 208 | "example" 209 | ], 210 | "summary": "custome header example", 211 | "parameters": [ 212 | { 213 | "type": "string", 214 | "description": "Authentication header", 215 | "name": "Authorization", 216 | "in": "header", 217 | "required": true 218 | } 219 | ], 220 | "responses": { 221 | "200": { 222 | "description": "", 223 | "schema": { 224 | "type": "string" 225 | } 226 | }, 227 | "400": { 228 | "description": "", 229 | "schema": { 230 | "type": "string" 231 | } 232 | }, 233 | "404": { 234 | "description": "", 235 | "schema": { 236 | "type": "string" 237 | } 238 | } 239 | } 240 | } 241 | }, 242 | "/api/v1/examples/ping": { 243 | "get": { 244 | "description": "do ping", 245 | "produces": [ 246 | "application/json" 247 | ], 248 | "tags": [ 249 | "example" 250 | ], 251 | "summary": "ping example", 252 | "responses": { 253 | "200": { 254 | "description": "", 255 | "schema": { 256 | "type": "string" 257 | } 258 | }, 259 | "default": { 260 | "description": "", 261 | "schema": { 262 | "type": "string" 263 | } 264 | } 265 | } 266 | } 267 | }, 268 | "/api/v1/examples/post": { 269 | "post": { 270 | "consumes": [ 271 | "application/json" 272 | ], 273 | "produces": [ 274 | "application/json" 275 | ], 276 | "parameters": [ 277 | { 278 | "name": "body", 279 | "in": "body", 280 | "schema": { 281 | "$ref": "#/definitions/model.Account" 282 | } 283 | } 284 | ], 285 | "responses": { 286 | "200": { 287 | "description": "", 288 | "schema": { 289 | "$ref": "#/definitions/model.Account" 290 | } 291 | }, 292 | "default": { 293 | "description": "", 294 | "schema": { 295 | "$ref": "#/definitions/main.Error" 296 | } 297 | } 298 | } 299 | } 300 | }, 301 | "/api/v1/examples/securities": { 302 | "get": { 303 | "security": [ 304 | { 305 | "ApiKeyAuth": [] 306 | } 307 | ], 308 | "description": "custome header", 309 | "produces": [ 310 | "application/json" 311 | ], 312 | "tags": [ 313 | "example" 314 | ], 315 | "summary": "custome header example", 316 | "parameters": [ 317 | { 318 | "type": "string", 319 | "description": "Authentication header", 320 | "name": "Authorization", 321 | "in": "header", 322 | "required": true 323 | } 324 | ], 325 | "responses": { 326 | "200": { 327 | "description": "", 328 | "schema": { 329 | "type": "string" 330 | } 331 | }, 332 | "400": { 333 | "description": "", 334 | "schema": { 335 | "type": "string" 336 | } 337 | }, 338 | "404": { 339 | "description": "", 340 | "schema": { 341 | "type": "string" 342 | } 343 | } 344 | } 345 | } 346 | } 347 | }, 348 | "definitions": { 349 | "main.Error": { 350 | "type": "object", 351 | "properties": { 352 | "error": { 353 | "type": "string" 354 | }, 355 | "error_debug": { 356 | "type": "string" 357 | }, 358 | "error_description": { 359 | "type": "string" 360 | }, 361 | "status_code": { 362 | "type": "integer" 363 | } 364 | } 365 | }, 366 | "model.Account": { 367 | "type": "object", 368 | "properties": { 369 | "id": { 370 | "type": "integer", 371 | "format": "int64", 372 | "example": 1 373 | }, 374 | "name": { 375 | "type": "string", 376 | "example": "account name" 377 | }, 378 | "some_number": { 379 | "type": "integer", 380 | "example": 1234 381 | }, 382 | "uuid": { 383 | "type": "string", 384 | "format": "uuid", 385 | "example": "550e8400-e29b-41d4-a716-446655440000" 386 | } 387 | } 388 | } 389 | }, 390 | "securityDefinitions": { 391 | "ApiKeyAuth": { 392 | "type": "apiKey", 393 | "name": "Authorization", 394 | "in": "header" 395 | } 396 | } 397 | } -------------------------------------------------------------------------------- /examples/docs/basic/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | main.Error: 3 | properties: 4 | error: 5 | type: string 6 | error_debug: 7 | type: string 8 | error_description: 9 | type: string 10 | status_code: 11 | type: integer 12 | type: object 13 | model.Account: 14 | properties: 15 | id: 16 | example: 1 17 | format: int64 18 | type: integer 19 | name: 20 | example: account name 21 | type: string 22 | some_number: 23 | example: 1234 24 | type: integer 25 | uuid: 26 | example: 550e8400-e29b-41d4-a716-446655440000 27 | format: uuid 28 | type: string 29 | type: object 30 | host: localhost:8080 31 | info: 32 | contact: 33 | email: support@swagger.io 34 | name: API Support 35 | url: http://www.swagger.io/support 36 | description: This is a sample celler server. 37 | license: 38 | name: Apache 2.0 39 | url: http://www.apache.org/licenses/LICENSE-2.0.html 40 | termsOfService: http://swagger.io/terms/ 41 | title: Swagger Example API 42 | version: "1.0" 43 | paths: 44 | /api/v1/examples/attribute: 45 | get: 46 | description: attribute 47 | parameters: 48 | - default: A 49 | description: string default 50 | in: query 51 | name: default 52 | type: string 53 | - description: int enums 54 | enum: 55 | - 1 56 | - 2 57 | - 3 58 | in: query 59 | name: enumint 60 | type: integer 61 | - description: int enums 62 | enum: 63 | - 1.1 64 | - 1.2 65 | - 1.3 66 | in: query 67 | name: enumnumber 68 | type: number 69 | - description: string enums 70 | enum: 71 | - A 72 | - B 73 | - C 74 | in: query 75 | name: enumstring 76 | type: string 77 | - description: int valid 78 | in: query 79 | maximum: 10 80 | minimum: 1 81 | name: int 82 | type: integer 83 | - description: string valid 84 | in: query 85 | maxLength: 10 86 | minLength: 5 87 | name: string 88 | type: string 89 | produces: 90 | - application/json 91 | responses: 92 | "200": 93 | description: answer 94 | schema: 95 | type: string 96 | "400": 97 | description: ok 98 | schema: 99 | type: string 100 | "404": 101 | description: ok 102 | schema: 103 | type: string 104 | summary: attribute example 105 | tags: 106 | - example 107 | /api/v1/examples/calc: 108 | get: 109 | parameters: 110 | - description: used for calc 111 | in: query 112 | name: val1 113 | required: true 114 | type: integer 115 | - description: used for calc 116 | in: query 117 | name: val2 118 | required: true 119 | type: integer 120 | produces: 121 | - application/json 122 | responses: 123 | "203": 124 | description: "" 125 | schema: 126 | type: string 127 | "400": 128 | description: "" 129 | schema: 130 | type: string 131 | "404": 132 | description: "" 133 | schema: 134 | type: string 135 | /api/v1/examples/groups/{group_id}/accounts/{account_id}: 136 | get: 137 | description: path params 138 | parameters: 139 | - description: Account ID 140 | in: path 141 | name: account_id 142 | required: true 143 | type: integer 144 | - description: Group ID 145 | in: path 146 | name: group_id 147 | required: true 148 | type: integer 149 | produces: 150 | - application/json 151 | responses: 152 | "200": 153 | description: "" 154 | schema: 155 | type: string 156 | "400": 157 | description: "" 158 | schema: 159 | type: string 160 | "404": 161 | description: "" 162 | schema: 163 | type: string 164 | summary: path params example 165 | tags: 166 | - example 167 | /api/v1/examples/header: 168 | get: 169 | description: custome header 170 | parameters: 171 | - description: Authentication header 172 | in: header 173 | name: Authorization 174 | required: true 175 | type: string 176 | produces: 177 | - application/json 178 | responses: 179 | "200": 180 | description: "" 181 | schema: 182 | type: string 183 | "400": 184 | description: "" 185 | schema: 186 | type: string 187 | "404": 188 | description: "" 189 | schema: 190 | type: string 191 | summary: custome header example 192 | tags: 193 | - example 194 | /api/v1/examples/ping: 195 | get: 196 | description: do ping 197 | produces: 198 | - application/json 199 | responses: 200 | "200": 201 | description: "" 202 | schema: 203 | type: string 204 | default: 205 | description: "" 206 | schema: 207 | type: string 208 | summary: ping example 209 | tags: 210 | - example 211 | /api/v1/examples/post: 212 | post: 213 | consumes: 214 | - application/json 215 | parameters: 216 | - in: body 217 | name: body 218 | schema: 219 | $ref: '#/definitions/model.Account' 220 | produces: 221 | - application/json 222 | responses: 223 | "200": 224 | description: "" 225 | schema: 226 | $ref: '#/definitions/model.Account' 227 | default: 228 | description: "" 229 | schema: 230 | $ref: '#/definitions/main.Error' 231 | /api/v1/examples/securities: 232 | get: 233 | description: custome header 234 | parameters: 235 | - description: Authentication header 236 | in: header 237 | name: Authorization 238 | required: true 239 | type: string 240 | produces: 241 | - application/json 242 | responses: 243 | "200": 244 | description: "" 245 | schema: 246 | type: string 247 | "400": 248 | description: "" 249 | schema: 250 | type: string 251 | "404": 252 | description: "" 253 | schema: 254 | type: string 255 | security: 256 | - ApiKeyAuth: [] 257 | summary: custome header example 258 | tags: 259 | - example 260 | securityDefinitions: 261 | ApiKeyAuth: 262 | in: header 263 | name: Authorization 264 | type: apiKey 265 | swagger: "2.0" 266 | -------------------------------------------------------------------------------- /examples/docs/celler/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | controller.Message: 3 | properties: 4 | message: 5 | example: message 6 | type: string 7 | type: object 8 | httputil.Error: 9 | properties: 10 | error: 11 | type: string 12 | error_debug: 13 | type: string 14 | error_description: 15 | type: string 16 | status_code: 17 | type: integer 18 | type: object 19 | model.Account: 20 | properties: 21 | id: 22 | example: 1 23 | format: int64 24 | type: integer 25 | name: 26 | example: account name 27 | type: string 28 | some_number: 29 | example: 1234 30 | type: integer 31 | uuid: 32 | example: 550e8400-e29b-41d4-a716-446655440000 33 | format: uuid 34 | type: string 35 | type: object 36 | model.Account2: 37 | properties: 38 | custom_type_4567: 39 | description: zzz 40 | items: 41 | $ref: '#/definitions/model.CustomType456' 42 | type: array 43 | custom_val_123: 44 | example: custom val 123 45 | type: string 46 | id: 47 | example: 113 48 | format: int64 49 | maximum: 195 50 | type: integer 51 | name: 52 | example: account name 53 | type: string 54 | some_number: 55 | example: 1234 56 | type: integer 57 | uuid: 58 | example: 550e8400-e29b-41d4-a716-446655440000 59 | format: uuid 60 | type: string 61 | type: object 62 | model.AddAccount: 63 | properties: 64 | name: 65 | example: account name 66 | type: string 67 | type: object 68 | model.Admin: 69 | properties: 70 | id: 71 | example: 1 72 | type: integer 73 | name: 74 | example: admin name 75 | type: string 76 | type: object 77 | model.Bottle: 78 | properties: 79 | account: 80 | $ref: '#/definitions/model.Account' 81 | id: 82 | example: 1 83 | type: integer 84 | name: 85 | example: bottle_name 86 | type: string 87 | type: object 88 | model.CustomType456: 89 | properties: 90 | zz: 91 | example: 34 92 | type: integer 93 | zzStr: 94 | example: zz string value 95 | type: string 96 | type: object 97 | model.UpdateAccount: 98 | properties: 99 | name: 100 | example: account name 101 | type: string 102 | type: object 103 | host: localhost:8080 104 | info: 105 | contact: 106 | email: support@swagger.io 107 | name: API Support 108 | url: http://www.swagger.io/support 109 | description: This is a sample celler server. 110 | license: 111 | name: Apache 2.0 112 | url: http://www.apache.org/licenses/LICENSE-2.0.html 113 | termsOfService: http://swagger.io/terms/ 114 | title: Swagger Example API 115 | version: "1.0" 116 | paths: 117 | /api/v1/accounts/: 118 | get: 119 | consumes: 120 | - application/json 121 | description: get accounts 122 | parameters: 123 | - description: name search by q 124 | format: email 125 | in: query 126 | name: q 127 | type: string 128 | produces: 129 | - application/json 130 | responses: 131 | "201": 132 | description: Created 133 | schema: 134 | items: 135 | $ref: '#/definitions/model.Account' 136 | type: array 137 | "202": 138 | description: Accepted 139 | schema: 140 | items: 141 | $ref: '#/definitions/model.Account' 142 | type: array 143 | "203": 144 | description: Non-Authoritative Information 145 | schema: 146 | items: 147 | $ref: '#/definitions/model.Account' 148 | type: array 149 | "400": 150 | description: Bad Request 151 | schema: 152 | $ref: '#/definitions/httputil.Error' 153 | "404": 154 | description: Not Found 155 | schema: 156 | $ref: '#/definitions/httputil.Error' 157 | "500": 158 | description: Internal Server Error 159 | schema: 160 | $ref: '#/definitions/httputil.Error' 161 | summary: List accounts 162 | tags: 163 | - accounts 164 | post: 165 | consumes: 166 | - application/json 167 | description: add by json account 168 | parameters: 169 | - description: Add account 170 | in: body 171 | name: account 172 | required: true 173 | schema: 174 | $ref: '#/definitions/model.AddAccount' 175 | produces: 176 | - application/json 177 | responses: 178 | "200": 179 | description: OK 180 | schema: 181 | $ref: '#/definitions/model.Account' 182 | "400": 183 | description: Bad Request 184 | schema: 185 | $ref: '#/definitions/httputil.Error' 186 | "404": 187 | description: Not Found 188 | schema: 189 | $ref: '#/definitions/httputil.Error' 190 | "500": 191 | description: Internal Server Error 192 | schema: 193 | $ref: '#/definitions/httputil.Error' 194 | summary: Add an account 195 | tags: 196 | - accounts 197 | /api/v1/accounts/{id}: 198 | delete: 199 | consumes: 200 | - application/json 201 | description: Delete by account ID 202 | parameters: 203 | - in: path 204 | name: id 205 | pattern: ^[0-9]+$ 206 | required: true 207 | type: integer 208 | produces: 209 | - application/json 210 | responses: 211 | "204": 212 | description: No Content 213 | schema: 214 | $ref: '#/definitions/model.Account' 215 | "400": 216 | description: Bad Request 217 | schema: 218 | $ref: '#/definitions/httputil.Error' 219 | "404": 220 | description: Not Found 221 | schema: 222 | $ref: '#/definitions/httputil.Error' 223 | "500": 224 | description: Internal Server Error 225 | schema: 226 | $ref: '#/definitions/httputil.Error' 227 | summary: Delete an account 228 | tags: 229 | - accounts 230 | get: 231 | consumes: 232 | - application/json 233 | description: get string by ID 234 | parameters: 235 | - description: Account ID 236 | in: path 237 | name: id 238 | required: true 239 | type: integer 240 | produces: 241 | - application/json 242 | responses: 243 | "200": 244 | description: OK 245 | schema: 246 | $ref: '#/definitions/model.Account' 247 | "400": 248 | description: Bad Request 249 | schema: 250 | $ref: '#/definitions/httputil.Error' 251 | "404": 252 | description: Not Found 253 | schema: 254 | $ref: '#/definitions/httputil.Error' 255 | "500": 256 | description: Internal Server Error 257 | schema: 258 | $ref: '#/definitions/httputil.Error' 259 | summary: Show an account 260 | tags: 261 | - accounts 262 | patch: 263 | consumes: 264 | - application/json 265 | description: Update by json account 266 | parameters: 267 | - description: Update account 268 | in: body 269 | name: account 270 | required: true 271 | schema: 272 | $ref: '#/definitions/model.UpdateAccount' 273 | - description: Account ID 274 | in: path 275 | name: id 276 | required: true 277 | type: integer 278 | produces: 279 | - application/json 280 | responses: 281 | "200": 282 | description: OK 283 | schema: 284 | $ref: '#/definitions/model.Account' 285 | "400": 286 | description: Bad Request 287 | schema: 288 | $ref: '#/definitions/httputil.Error' 289 | "404": 290 | description: Not Found 291 | schema: 292 | $ref: '#/definitions/httputil.Error' 293 | "500": 294 | description: Internal Server Error 295 | schema: 296 | $ref: '#/definitions/httputil.Error' 297 | summary: Update an account 298 | tags: 299 | - accounts 300 | /api/v1/accounts/{id}/images: 301 | post: 302 | consumes: 303 | - multipart/form-data 304 | description: Upload file 305 | parameters: 306 | - description: account image 307 | in: formData 308 | name: file 309 | required: true 310 | type: file 311 | - description: Account ID 312 | in: path 313 | name: id 314 | required: true 315 | type: integer 316 | produces: 317 | - application/json 318 | responses: 319 | "200": 320 | description: OK 321 | schema: 322 | $ref: '#/definitions/controller.Message' 323 | "400": 324 | description: Bad Request 325 | schema: 326 | $ref: '#/definitions/httputil.Error' 327 | "404": 328 | description: Not Found 329 | schema: 330 | $ref: '#/definitions/httputil.Error' 331 | "500": 332 | description: Internal Server Error 333 | schema: 334 | $ref: '#/definitions/httputil.Error' 335 | summary: Upload account image 336 | tags: 337 | - accounts 338 | /api/v1/admin/auth: 339 | post: 340 | consumes: 341 | - application/json 342 | description: get admin info 343 | parameters: 344 | - in: body 345 | name: body 346 | schema: 347 | type: object 348 | produces: 349 | - application/json 350 | responses: 351 | "200": 352 | description: "" 353 | schema: 354 | $ref: '#/definitions/model.Admin' 355 | "400": 356 | description: Bad Request 357 | schema: 358 | $ref: '#/definitions/httputil.Error' 359 | "401": 360 | description: Unauthorized 361 | schema: 362 | $ref: '#/definitions/httputil.Error' 363 | "404": 364 | description: Not Found 365 | schema: 366 | $ref: '#/definitions/httputil.Error' 367 | "500": 368 | description: Internal Server Error 369 | schema: 370 | $ref: '#/definitions/httputil.Error' 371 | security: 372 | - ApiKeyAuth: [] 373 | summary: Auth admin 374 | tags: 375 | - accounts 376 | - admin 377 | /api/v1/bottles/: 378 | get: 379 | consumes: 380 | - application/json 381 | description: get bottles 382 | produces: 383 | - application/json 384 | responses: 385 | "200": 386 | description: "" 387 | schema: 388 | items: 389 | $ref: '#/definitions/model.Bottle' 390 | type: array 391 | "201": 392 | description: "" 393 | schema: 394 | items: 395 | $ref: '#/definitions/model.Bottle' 396 | type: array 397 | "202": 398 | description: "" 399 | schema: 400 | items: 401 | $ref: '#/definitions/model.Bottle' 402 | type: array 403 | "400": 404 | description: "" 405 | schema: 406 | $ref: '#/definitions/httputil.Error' 407 | "404": 408 | description: "" 409 | schema: 410 | $ref: '#/definitions/httputil.Error' 411 | "500": 412 | description: "" 413 | schema: 414 | $ref: '#/definitions/httputil.Error' 415 | summary: List bottles 416 | tags: 417 | - bottles 418 | /api/v1/bottles/{id}: 419 | get: 420 | consumes: 421 | - application/json 422 | description: get string by ID 423 | operationId: get-string-by-int 424 | parameters: 425 | - description: Bottle ID 426 | in: path 427 | name: id 428 | required: true 429 | type: integer 430 | produces: 431 | - application/json 432 | responses: 433 | "200": 434 | description: OK 435 | schema: 436 | $ref: '#/definitions/model.Bottle' 437 | "400": 438 | description: Bad Request 439 | schema: 440 | $ref: '#/definitions/httputil.Error' 441 | "404": 442 | description: Not Found 443 | schema: 444 | $ref: '#/definitions/httputil.Error' 445 | "500": 446 | description: Internal Server Error 447 | schema: 448 | $ref: '#/definitions/httputil.Error' 449 | summary: Show a bottle 450 | tags: 451 | - bottles 452 | /api/v1/examples/attribute: 453 | get: 454 | consumes: 455 | - application/json 456 | description: attribute 457 | parameters: 458 | - default: A 459 | description: string default 460 | in: query 461 | name: default 462 | type: string 463 | - description: int enums 464 | enum: 465 | - 1 466 | - 2 467 | - 3 468 | in: query 469 | name: enumint 470 | type: integer 471 | - description: int enums 472 | enum: 473 | - 1.1 474 | - 1.2 475 | - 1.3 476 | in: query 477 | name: enumnumber 478 | type: number 479 | - description: string enums 480 | enum: 481 | - A 482 | - B 483 | - C 484 | in: query 485 | name: enumstring 486 | type: string 487 | - description: int valid 488 | in: query 489 | maximum: 10 490 | minimum: 1 491 | name: int 492 | type: integer 493 | - description: string valid 494 | in: query 495 | maxLength: 10 496 | minLength: 5 497 | name: string 498 | type: string 499 | produces: 500 | - application/json 501 | responses: 502 | "200": 503 | description: answer 504 | schema: 505 | type: string 506 | "400": 507 | description: ok 508 | schema: 509 | type: string 510 | "404": 511 | description: ok 512 | schema: 513 | type: string 514 | "500": 515 | description: ok 516 | schema: 517 | type: string 518 | summary: attribute example 519 | tags: 520 | - example 521 | post: 522 | consumes: 523 | - application/json 524 | description: post request example 525 | parameters: 526 | - in: body 527 | name: body 528 | schema: 529 | $ref: '#/definitions/model.Account' 530 | produces: 531 | - application/json 532 | responses: 533 | "200": 534 | description: "" 535 | schema: 536 | $ref: '#/definitions/model.Account2' 537 | default: 538 | description: "" 539 | schema: 540 | $ref: '#/definitions/httputil.Error' 541 | summary: post request example 542 | /api/v1/examples/calc: 543 | get: 544 | consumes: 545 | - application/json 546 | description: plus 547 | parameters: 548 | - description: used for calc 549 | in: query 550 | name: val1 551 | required: true 552 | type: integer 553 | - description: used for calc 554 | in: query 555 | name: val2 556 | required: true 557 | type: integer 558 | produces: 559 | - application/json 560 | responses: 561 | "200": 562 | description: answer 563 | schema: 564 | type: integer 565 | "400": 566 | description: ok 567 | schema: 568 | type: string 569 | "404": 570 | description: ok 571 | schema: 572 | type: string 573 | "500": 574 | description: ok 575 | schema: 576 | type: string 577 | summary: calc example 578 | tags: 579 | - example 580 | /api/v1/examples/groups/{group_id}/accounts/{account_id}: 581 | get: 582 | consumes: 583 | - application/json 584 | description: path params 585 | parameters: 586 | - description: Account ID 587 | in: path 588 | name: account_id 589 | required: true 590 | type: integer 591 | - description: Group ID 592 | in: path 593 | name: group_id 594 | required: true 595 | type: integer 596 | produces: 597 | - application/json 598 | responses: 599 | "200": 600 | description: answer 601 | schema: 602 | type: string 603 | "400": 604 | description: ok 605 | schema: 606 | type: string 607 | "404": 608 | description: ok 609 | schema: 610 | type: string 611 | "500": 612 | description: ok 613 | schema: 614 | type: string 615 | summary: path params example 616 | tags: 617 | - example 618 | /api/v1/examples/header: 619 | get: 620 | consumes: 621 | - application/json 622 | description: custome header 623 | parameters: 624 | - description: Authentication header 625 | in: header 626 | name: Authorization 627 | required: true 628 | type: string 629 | produces: 630 | - application/json 631 | responses: 632 | "200": 633 | description: answer 634 | schema: 635 | type: string 636 | "400": 637 | description: ok 638 | schema: 639 | type: string 640 | "404": 641 | description: ok 642 | schema: 643 | type: string 644 | "500": 645 | description: ok 646 | schema: 647 | type: string 648 | summary: custome header example 649 | tags: 650 | - example 651 | /api/v1/examples/ping: 652 | get: 653 | consumes: 654 | - application/json 655 | description: do ping 656 | produces: 657 | - application/json 658 | responses: 659 | "200": 660 | description: pong 661 | schema: 662 | type: string 663 | "400": 664 | description: ok 665 | schema: 666 | type: string 667 | "404": 668 | description: ok 669 | schema: 670 | type: string 671 | "500": 672 | description: ok 673 | schema: 674 | type: string 675 | summary: ping example 676 | tags: 677 | - example 678 | /api/v1/examples/securities: 679 | get: 680 | consumes: 681 | - application/json 682 | description: custome header 683 | parameters: 684 | - description: Authentication header 685 | in: header 686 | name: Authorization 687 | required: true 688 | type: string 689 | produces: 690 | - application/json 691 | responses: 692 | "200": 693 | description: answer 694 | schema: 695 | type: string 696 | "400": 697 | description: ok 698 | schema: 699 | type: string 700 | "404": 701 | description: ok 702 | schema: 703 | type: string 704 | "500": 705 | description: ok 706 | schema: 707 | type: string 708 | security: 709 | - ApiKeyAuth: [] 710 | - OAuth2Implicit: 711 | - admin 712 | - write 713 | summary: custome header example 714 | tags: 715 | - example 716 | securityDefinitions: 717 | ApiKeyAuth: 718 | in: header 719 | name: Authorization 720 | type: apiKey 721 | BasicAuth: 722 | type: basic 723 | OAuth2AccessToken: 724 | authorizationUrl: https://example.com/oauth/authorize 725 | flow: accessCode 726 | scopes: 727 | admin: Grants read and write access to administrative information 728 | tokenUrl: https://example.com/oauth/token 729 | type: oauth2 730 | OAuth2Application: 731 | description: Use with the OAuth2 Implicit Grant to retrieve a token 732 | flow: application 733 | scopes: 734 | admin: Grants read and write access to administrative information 735 | write: Grants write access 736 | tokenUrl: https://example.com/oauth/token 737 | type: oauth2 738 | OAuth2Implicit: 739 | authorizationUrl: https://example.com/oauth/authorize 740 | description: Use with the OAuth2 Implicit Grant to retrieve a token 741 | flow: implicit 742 | scopes: 743 | admin: Grants read and write access to administrative information 744 | write: Grants write access 745 | type: oauth2 746 | OAuth2Password: 747 | flow: password 748 | scopes: 749 | admin: Grants read and write access to administrative information 750 | read: Grants read access 751 | write: Grants write access 752 | tokenUrl: https://example.com/oauth/token 753 | type: oauth2 754 | swagger: "2.0" 755 | -------------------------------------------------------------------------------- /examples/gorilla/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | _ "github.com/go-chai/chai/examples/docs/basic" // This is required to be able to serve the stored swagger spec in prod 9 | "github.com/go-chai/chai/examples/shared/model" 10 | chai "github.com/go-chai/chai/gorilla" 11 | "github.com/go-chai/chai/openapi2" 12 | "github.com/go-chi/chi/v5" 13 | "github.com/go-openapi/spec" 14 | "github.com/gorilla/mux" 15 | httpSwagger "github.com/swaggo/http-swagger" 16 | ) 17 | 18 | func main() { 19 | r := mux.NewRouter() 20 | 21 | chai.Post(r, "/api/v1/examples/post", PostHandler) 22 | chai.Get(r, "/api/v1/examples/ping", PingHandler) 23 | chai.Get(r, "/api/v1/examples/calc", CalcHandler) 24 | chai.Get(r, "/api/v1/examples/groups/{group_id}/accounts/{account_id}", PathParamsHandler) 25 | chai.Get(r, "/api/v1/examples/header", HeaderHandler) 26 | chai.Get(r, "/api/v1/examples/securities", SecuritiesHandler) 27 | chai.Get(r, "/api/v1/examples/attribute", AttributeHandler) 28 | 29 | // This must be used only during development to generate the swagger spec 30 | docs, err := chai.OpenAPI2(r) 31 | if err != nil { 32 | panic(fmt.Sprintf("failed to generate the swagger spec: %+v", err)) 33 | } 34 | 35 | // This should be used in prod to serve the swagger spec 36 | r.PathPrefix("/swagger/").Handler(httpSwagger.Handler( 37 | httpSwagger.URL("http://localhost:8080/swagger/doc.json"), //The url pointing to API definition 38 | )) 39 | 40 | addCustomDocs(docs) 41 | 42 | openapi2.LogYAML(docs) 43 | 44 | // This must be used only during development to store the swagger spec 45 | err = openapi2.WriteDocs(docs, &openapi2.GenConfig{ 46 | OutputDir: "examples/docs/basic", 47 | }) 48 | if err != nil { 49 | panic(fmt.Sprintf("failed to write the swagger spec: %+v", err)) 50 | } 51 | 52 | fmt.Println("The swagger spec is available at http://localhost:8080/swagger/") 53 | 54 | http.ListenAndServe(":8080", r) 55 | } 56 | 57 | type Error struct { 58 | Message string `json:"error"` 59 | ErrorDebug string `json:"error_debug,omitempty"` 60 | ErrorDescription string `json:"error_description,omitempty"` 61 | StatusCode int `json:"status_code,omitempty"` 62 | } 63 | 64 | func (e *Error) Error() string { 65 | return e.Message 66 | } 67 | 68 | func PostHandler(account *model.Account, w http.ResponseWriter, r *http.Request) (*model.Account, int, *Error) { 69 | return account, http.StatusOK, nil 70 | } 71 | 72 | // @Param val1 query int true "used for calc" 73 | // @Param val2 query int true "used for calc" 74 | // @Success 203 75 | // @Failure 400,404 76 | func CalcHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 77 | val1, err := strconv.Atoi(r.URL.Query().Get("val1")) 78 | if err != nil { 79 | return "", http.StatusBadRequest, err 80 | } 81 | val2, err := strconv.Atoi(r.URL.Query().Get("val2")) 82 | if err != nil { 83 | return "", http.StatusBadRequest, err 84 | } 85 | return fmt.Sprintf("%d", val1*val2), http.StatusOK, nil 86 | } 87 | 88 | // PingExample godoc 89 | // @Summary ping example 90 | // @Description do ping 91 | // @Tags example 92 | func PingHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 93 | return "pong", http.StatusOK, nil 94 | } 95 | 96 | // PathParamsHandler godoc 97 | // @Summary path params example 98 | // @Description path params 99 | // @Tags example 100 | // @Param group_id path int true "Group ID" 101 | // @Param account_id path int true "Account ID" 102 | // @Failure 400,404 103 | func PathParamsHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 104 | groupID, err := strconv.Atoi(chi.URLParam(r, "group_id")) 105 | if err != nil { 106 | return "", http.StatusBadRequest, err 107 | } 108 | accountID, err := strconv.Atoi(chi.URLParam(r, "account_id")) 109 | if err != nil { 110 | return "", http.StatusBadRequest, err 111 | } 112 | 113 | return fmt.Sprintf("group_id=%d account_id=%d", groupID, accountID), http.StatusOK, nil 114 | } 115 | 116 | // HeaderHandler godoc 117 | // @Summary custome header example 118 | // @Description custome header 119 | // @Tags example 120 | // @Param Authorization header string true "Authentication header" 121 | // @Failure 400,404 122 | func HeaderHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 123 | return r.Header.Get("Authorization"), http.StatusOK, nil 124 | } 125 | 126 | // SecuritiesHandler godoc 127 | // @Summary custome header example 128 | // @Description custome header 129 | // @Tags example 130 | // @Param Authorization header string true "Authentication header" 131 | // @Failure 400,404 132 | // @Security ApiKeyAuth 133 | func SecuritiesHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 134 | return "ok", http.StatusOK, nil 135 | } 136 | 137 | // AttributeHandler godoc 138 | // @Summary attribute example 139 | // @Description attribute 140 | // @Tags example 141 | // @Param enumstring query string false "string enums" Enums(A, B, C) 142 | // @Param enumint query int false "int enums" Enums(1, 2, 3) 143 | // @Param enumnumber query number false "int enums" Enums(1.1, 1.2, 1.3) 144 | // @Param string query string false "string valid" minlength(5) maxlength(10) 145 | // @Param int query int false "int valid" minimum(1) maximum(10) 146 | // @Param default query string false "string default" default(A) 147 | // @Success 200 "answer" 148 | // @Failure 400,404 "ok" 149 | func AttributeHandler(w http.ResponseWriter, r *http.Request) (string, int, error) { 150 | return fmt.Sprintf("enumstring=%s enumint=%s enumnumber=%s string=%s int=%s default=%s", 151 | r.URL.Query().Get("enumstring"), 152 | r.URL.Query().Get("enumint"), 153 | r.URL.Query().Get("enumnumber"), 154 | r.URL.Query().Get("string"), 155 | r.URL.Query().Get("int"), 156 | r.URL.Query().Get("default"), 157 | ), http.StatusOK, nil 158 | } 159 | 160 | func addCustomDocs(docs *spec.Swagger) { 161 | docs.Swagger = "2.0" 162 | docs.Host = "localhost:8080" 163 | docs.Info = &spec.Info{ 164 | InfoProps: spec.InfoProps{ 165 | Description: "This is a sample celler server.", 166 | Title: "Swagger Example API", 167 | TermsOfService: "http://swagger.io/terms/", 168 | Contact: &spec.ContactInfo{ 169 | ContactInfoProps: spec.ContactInfoProps{ 170 | Name: "API Support", 171 | URL: "http://www.swagger.io/support", 172 | Email: "support@swagger.io", 173 | }, 174 | }, 175 | License: &spec.License{ 176 | LicenseProps: spec.LicenseProps{ 177 | Name: "Apache 2.0", 178 | URL: "http://www.apache.org/licenses/LICENSE-2.0.html", 179 | }, 180 | }, 181 | Version: "1.0", 182 | }, 183 | } 184 | docs.SecurityDefinitions = map[string]*spec.SecurityScheme{ 185 | "ApiKeyAuth": { 186 | SecuritySchemeProps: spec.SecuritySchemeProps{ 187 | Type: "apiKey", 188 | In: "header", 189 | Name: "Authorization", 190 | }, 191 | }, 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /examples/gorilla/celler/README.md: -------------------------------------------------------------------------------- 1 | # Celler example for github.com/go-chai/chai 2 | 3 | Run the app 4 | 5 | ```console 6 | $ go run -gcflags -N ./examples/gorilla/celler/main.go 7 | ``` 8 | 9 | [open swagger](http://localhost:8080/swagger/index.html) 10 | -------------------------------------------------------------------------------- /examples/gorilla/celler/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/ghodss/yaml" 9 | "github.com/gorilla/mux" 10 | 11 | _ "github.com/go-chai/chai/examples/docs/celler" // This is required to be able to serve the stored swagger spec in prod 12 | "github.com/go-chai/chai/examples/shared/controller" 13 | "github.com/go-chai/chai/examples/shared/httputil" 14 | chai "github.com/go-chai/chai/gorilla" 15 | "github.com/go-chai/chai/openapi2" 16 | "github.com/go-openapi/spec" 17 | httpSwagger "github.com/swaggo/http-swagger" 18 | ) 19 | 20 | func main() { 21 | r := mux.NewRouter() 22 | 23 | c := controller.NewController() 24 | 25 | chai.Get(r, "/api/v1/accounts/{id}", c.ShowAccount) 26 | chai.Get(r, "/api/v1/accounts/", c.ListAccounts) 27 | chai.Post(r, "/api/v1/accounts/", c.AddAccount) 28 | r.HandleFunc("/api/v1/accounts/{id}", c.DeleteAccount).Methods(http.MethodDelete) 29 | r.HandleFunc("/api/v1/accounts/{id}", c.UpdateAccount).Methods(http.MethodPatch) 30 | r.HandleFunc("/api/v1/accounts/{id}/images", c.UploadAccountImage).Methods(http.MethodPost) 31 | chai.Get(r, "/api/v1/bottles/{id}", c.ShowBottle) 32 | chai.Get(r, "/api/v1/bottles/", c.ListBottles) 33 | chai.Get(r, "/api/v1/bottles/", c.ListBottles) 34 | 35 | authMux := r.Path("/").Subrouter() 36 | authMux.Use(auth) 37 | 38 | chai.Post(authMux, "/api/v1/admin/auth", c.Auth) 39 | 40 | chai.Get(r, "/api/v1/examples/ping", c.PingExample) 41 | chai.Get(r, "/api/v1/examples/calc", c.CalcExample) 42 | // chai.Get(r, "/api/v1/examples/group{s/{gro}up_id}/accounts/{account_id}", c.CalcExample) 43 | chai.Get(r, "/api/v1/examples/groups/{group_id}/accounts/{account_id}", c.PathParamsExample) 44 | chai.Get(r, "/api/v1/examples/header", c.HeaderExample) 45 | chai.Get(r, "/api/v1/examples/securities", c.SecuritiesExample) 46 | chai.Get(r, "/api/v1/examples/attribute", c.AttributeExample) 47 | chai.Post(r, "/api/v1/examples/attribute", c.PostExample) 48 | 49 | // This must be used only during development to generate the swagger spec 50 | docs, err := chai.OpenAPI2(r) 51 | if err != nil { 52 | panic(fmt.Sprintf("failed to generate the swagger spec: %+v", err)) 53 | } 54 | 55 | // This should be used in prod to serve the swagger spec 56 | r.PathPrefix("/swagger/").Handler(httpSwagger.Handler( 57 | httpSwagger.URL("http://localhost:8080/swagger/doc.json"), //The url pointing to API definition 58 | )) 59 | 60 | addCustomDocs(docs) 61 | 62 | LogYAML(docs) 63 | 64 | // This must be used only during development to store the swagger spec 65 | err = openapi2.WriteDocs(docs, &openapi2.GenConfig{ 66 | OutputDir: "examples/docs/celler", 67 | }) 68 | if err != nil { 69 | panic(fmt.Sprintf("failed to write the swagger spec: %+v", err)) 70 | } 71 | 72 | fmt.Println("The swagger spec is available at http://localhost:8080/swagger/") 73 | 74 | http.ListenAndServe(":8080", r) 75 | } 76 | 77 | func addCustomDocs(docs *spec.Swagger) { 78 | docs.Swagger = "2.0" 79 | docs.Host = "localhost:8080" 80 | docs.Info = &spec.Info{ 81 | InfoProps: spec.InfoProps{ 82 | Description: "This is a sample celler server.", 83 | Title: "Swagger Example API", 84 | TermsOfService: "http://swagger.io/terms/", 85 | Contact: &spec.ContactInfo{ 86 | ContactInfoProps: spec.ContactInfoProps{ 87 | Name: "API Support", 88 | URL: "http://www.swagger.io/support", 89 | Email: "support@swagger.io", 90 | }, 91 | }, 92 | License: &spec.License{ 93 | LicenseProps: spec.LicenseProps{ 94 | Name: "Apache 2.0", 95 | URL: "http://www.apache.org/licenses/LICENSE-2.0.html", 96 | }, 97 | }, 98 | Version: "1.0", 99 | }, 100 | } 101 | docs.SecurityDefinitions = map[string]*spec.SecurityScheme{ 102 | "BasicAuth": { 103 | SecuritySchemeProps: spec.SecuritySchemeProps{ 104 | Type: "basic", 105 | }, 106 | }, 107 | "ApiKeyAuth": { 108 | SecuritySchemeProps: spec.SecuritySchemeProps{ 109 | Type: "apiKey", 110 | In: "header", 111 | Name: "Authorization", 112 | }, 113 | }, 114 | "OAuth2Implicit": { 115 | SecuritySchemeProps: spec.SecuritySchemeProps{ 116 | Description: "Use with the OAuth2 Implicit Grant to retrieve a token", 117 | Type: "oauth2", 118 | Flow: "implicit", 119 | AuthorizationURL: "https://example.com/oauth/authorize", 120 | TokenURL: "", 121 | Scopes: map[string]string{ 122 | "admin": "Grants read and write access to administrative information", 123 | "write": "Grants write access", 124 | }, 125 | }, 126 | }, 127 | "OAuth2Application": { 128 | SecuritySchemeProps: spec.SecuritySchemeProps{ 129 | Description: "Use with the OAuth2 Implicit Grant to retrieve a token", 130 | Type: "oauth2", 131 | Flow: "application", 132 | TokenURL: "https://example.com/oauth/token", 133 | Scopes: map[string]string{ 134 | "admin": "Grants read and write access to administrative information", 135 | "write": "Grants write access", 136 | }, 137 | }, 138 | }, 139 | 140 | "OAuth2Password": { 141 | SecuritySchemeProps: spec.SecuritySchemeProps{ 142 | Type: "oauth2", 143 | Flow: "password", 144 | TokenURL: "https://example.com/oauth/token", 145 | Scopes: map[string]string{ 146 | "admin": "Grants read and write access to administrative information", 147 | "write": "Grants write access", 148 | "read": "Grants read access", 149 | }, 150 | }, 151 | }, 152 | "OAuth2AccessToken": { 153 | SecuritySchemeProps: spec.SecuritySchemeProps{ 154 | Type: "oauth2", 155 | Flow: "accessCode", 156 | AuthorizationURL: "https://example.com/oauth/authorize", 157 | TokenURL: "https://example.com/oauth/token", 158 | Scopes: map[string]string{ 159 | "admin": "Grants read and write access to administrative information", 160 | }, 161 | }, 162 | }, 163 | } 164 | } 165 | 166 | func auth(next http.Handler) http.Handler { 167 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 168 | if len(r.Header.Get("Authorization")) == 0 { 169 | httputil.NewError(w, http.StatusUnauthorized, errors.New("Authorization is required Header")) 170 | return 171 | } 172 | next.ServeHTTP(w, r) 173 | }) 174 | } 175 | 176 | func LogYAML(v any) { 177 | bytes, err := yaml.Marshal(v) 178 | if err != nil { 179 | panic(err) 180 | } 181 | 182 | fmt.Println(string(bytes)) 183 | 184 | return 185 | } 186 | -------------------------------------------------------------------------------- /examples/shared/controller/accounts.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/go-chai/chai/examples/shared/httputil" 9 | "github.com/go-chai/chai/examples/shared/model" 10 | "github.com/go-chi/chi/v5" 11 | "github.com/gofrs/uuid" 12 | ) 13 | 14 | // ShowAccount godoc 15 | // @Summary Show an account 16 | // @Description get string by ID 17 | // @Tags accounts 18 | // @Accept json 19 | // @Produce json 20 | // @Param id path int true "Account ID" 21 | // @Success 200 {object} model.Account 22 | // @Failure 400 {object} httputil.Error 23 | // @Failure 404 {object} httputil.Error 24 | // @Failure 500 {object} httputil.Error 25 | func (c *Controller) ShowAccount(w http.ResponseWriter, r *http.Request) (*model.Account, int, error) { 26 | id := chi.URLParam(r, "id") 27 | aid, err := strconv.Atoi(id) 28 | if err != nil { 29 | return nil, http.StatusBadRequest, err 30 | } 31 | account, err := model.AccountOne(aid) 32 | if err != nil { 33 | return nil, http.StatusNotFound, err 34 | } 35 | 36 | return &account, http.StatusOK, nil 37 | } 38 | 39 | // ListAccounts godoc 40 | // @Summary List accounts 41 | // @Description get accounts 42 | // @Tags accounts 43 | // @Accept json 44 | // @Produce json 45 | // @Param q query string false "name search by q" Format(email) 46 | // @Success 201 {array} model.Account 47 | // @Success 202 {array} model.Account 48 | // @Success 203 {array} model.Account 49 | // @Failure 400 {object} httputil.Error 50 | // @Failure 404 {object} httputil.Error 51 | // @Failure 500 {object} httputil.Error 52 | // @Router /accounts [get] 53 | func (c *Controller) ListAccounts(w http.ResponseWriter, r *http.Request) (*[]model.Account, int, error) { 54 | q := r.URL.Query().Get("q") 55 | accounts, err := model.AccountsAll(q) 56 | if err != nil { 57 | return nil, http.StatusNotFound, err 58 | } 59 | 60 | return &accounts, http.StatusOK, nil 61 | } 62 | 63 | // AddAccount godoc 64 | // @Summary Add an account 65 | // @Description add by json account 66 | // @Tags accounts 67 | // @Accept json 68 | // @Produce json 69 | // @Param account body model.AddAccount true "Add account" 70 | // @Success 200 {object} model.Account 71 | // @Failure 400 {object} httputil.Error 72 | // @Failure 404 {object} httputil.Error 73 | // @Failure 500 {object} httputil.Error 74 | // @Router /accounts [post] 75 | func (c *Controller) AddAccount(addAccount *model.AddAccount, w http.ResponseWriter, r *http.Request) (*model.Account, int, error) { 76 | if err := addAccount.Validation(); err != nil { 77 | return nil, http.StatusBadRequest, err 78 | } 79 | account := model.Account{ 80 | Name: addAccount.Name, 81 | } 82 | lastID, err := account.Insert() 83 | if err != nil { 84 | return nil, http.StatusBadRequest, err 85 | } 86 | account.ID = lastID 87 | 88 | return &account, http.StatusOK, nil 89 | } 90 | 91 | // UpdateAccount godoc 92 | // @Summary Update an account 93 | // @Description Update by json account 94 | // @Tags accounts 95 | // @Accept json 96 | // @Produce json 97 | // @Param id path int true "Account ID" 98 | // @Param account body model.UpdateAccount true "Update account" 99 | // @Success 200 {object} model.Account 100 | // @Failure 400 {object} httputil.Error 101 | // @Failure 404 {object} httputil.Error 102 | // @Failure 500 {object} httputil.Error 103 | // @Router /accounts/{id} [patch] 104 | func (c *Controller) UpdateAccount(w http.ResponseWriter, r *http.Request) { 105 | id := chi.URLParam(r, "id") 106 | aid, err := strconv.Atoi(id) 107 | if httputil.NewError(w, http.StatusBadRequest, err) { 108 | return 109 | } 110 | updateAccount := new(model.UpdateAccount) 111 | err = httputil.Decode(r, updateAccount) 112 | if httputil.NewError(w, http.StatusBadRequest, err) { 113 | return 114 | } 115 | account := model.Account{ 116 | ID: aid, 117 | Name: updateAccount.Name, 118 | UUID: uuid.Must(uuid.NewV4()), 119 | } 120 | err = account.Update() 121 | if httputil.NewError(w, http.StatusNotFound, err) { 122 | return 123 | } 124 | httputil.Respond(w, r, account) 125 | } 126 | 127 | // DeleteAccount godoc 128 | // @Summary Delete an account 129 | // @Description Delete by account ID 130 | // @Tags accounts 131 | // @Accept json 132 | // @Produce json 133 | // @Success 204 {object} model.Account 134 | // @Failure 400 {object} httputil.Error 135 | // @Failure 404 {object} httputil.Error 136 | // @Failure 500 {object} httputil.Error 137 | func (c *Controller) DeleteAccount(w http.ResponseWriter, r *http.Request) { 138 | id := chi.URLParam(r, "id") 139 | aid, err := strconv.Atoi(id) 140 | if httputil.NewError(w, http.StatusBadRequest, err) { 141 | return 142 | } 143 | err = model.Delete(aid) 144 | if httputil.NewError(w, http.StatusNotFound, err) { 145 | return 146 | } 147 | httputil.Respond(w, r, struct{}{}) 148 | } 149 | 150 | // UploadAccountImage godoc 151 | // @Summary Upload account image 152 | // @Description Upload file 153 | // @Tags accounts 154 | // @Accept multipart/form-data 155 | // @Produce json 156 | // @Param id path int true "Account ID" 157 | // @Param file formData file true "account image" 158 | // @Success 200 {object} Message 159 | // @Failure 400 {object} httputil.Error 160 | // @Failure 404 {object} httputil.Error 161 | // @Failure 500 {object} httputil.Error 162 | func (c *Controller) UploadAccountImage(w http.ResponseWriter, r *http.Request) { 163 | id, err := strconv.Atoi(chi.URLParam(r, "id")) 164 | if httputil.NewError(w, http.StatusBadRequest, err) { 165 | return 166 | } 167 | _, h, err := r.FormFile("file") 168 | if httputil.NewError(w, http.StatusBadRequest, err) { 169 | return 170 | } 171 | 172 | httputil.Respond(w, r, Message{Message: fmt.Sprintf("upload complete userID=%d filename=%s", id, h.Filename)}) 173 | } 174 | 175 | // Admin example 176 | type Admin struct { 177 | ID int `json:"id" example:"1"` 178 | Name string `json:"name" example:"admin name"` 179 | } 180 | -------------------------------------------------------------------------------- /examples/shared/controller/admin.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/go-chai/chai/examples/shared/model" 9 | ) 10 | 11 | // Auth godoc 12 | // @Summary Auth admin 13 | // @Description get admin info 14 | // @Tags accounts,admin 15 | // @Accept json 16 | // @Produce json 17 | // @Success 200 18 | // @Failure 400 {object} httputil.Error 19 | // @Failure 401 {object} httputil.Error 20 | // @Failure 404 {object} httputil.Error 21 | // @Failure 500 {object} httputil.Error 22 | // @Security ApiKeyAuth 23 | // @Router /admin/auth [post] 24 | func (c *Controller) Auth(m any, w http.ResponseWriter, r *http.Request) (*model.Admin, int, error) { 25 | authHeader := r.Header.Get("Authorization") 26 | if len(authHeader) == 0 { 27 | return nil, http.StatusBadRequest, errors.New("please set Header Authorization") 28 | } 29 | if authHeader != "admin" { 30 | return nil, http.StatusUnauthorized, fmt.Errorf("this user isn't authorized to operation key=%s expected=admin", authHeader) 31 | } 32 | 33 | return &model.Admin{ 34 | ID: 1, 35 | Name: "admin", 36 | }, http.StatusOK, nil 37 | } 38 | -------------------------------------------------------------------------------- /examples/shared/controller/bottles.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/go-chi/chi/v5" 8 | 9 | "github.com/go-chai/chai/examples/shared/httputil" 10 | "github.com/go-chai/chai/examples/shared/model" 11 | ) 12 | 13 | // ShowBottle godoc 14 | // @Summary Show a bottle 15 | // @Description get string by ID 16 | // @ID get-string-by-int 17 | // @Tags bottles 18 | // @Accept json 19 | // @Produce json 20 | // @Param id path int true "Bottle ID" 21 | // @Success 200 {object} model.Bottle 22 | // @Failure 400 {object} httputil.Error 23 | // @Failure 404 {object} httputil.Error 24 | // @Failure 500 {object} httputil.Error 25 | func (c *Controller) ShowBottle(w http.ResponseWriter, r *http.Request) (*model.Bottle, int, error) { 26 | id := chi.URLParam(r, "id") 27 | bid, err := strconv.Atoi(id) 28 | if err != nil { 29 | return nil, http.StatusBadRequest, err 30 | } 31 | bottle, err := model.BottleOne(bid) 32 | if err != nil { 33 | return nil, http.StatusNotFound, err 34 | } 35 | return bottle, http.StatusOK, nil 36 | } 37 | 38 | // ListBottles godoc 39 | // @Summary List bottles 40 | // @Description get bottles 41 | // @Tags bottles 42 | // @Accept json 43 | // @Produce json 44 | // @Success 200,201,202 45 | // @Failure 400,404,500 46 | func (c *Controller) ListBottles(w http.ResponseWriter, r *http.Request) (*[]model.Bottle, int, *httputil.Error) { 47 | bottles, err := model.BottlesAll() 48 | if err != nil { 49 | return nil, http.StatusNotFound, &httputil.Error{ 50 | StatusCode: http.StatusNotFound, 51 | Message: err.Error(), 52 | } 53 | } 54 | return &bottles, http.StatusOK, nil 55 | } 56 | -------------------------------------------------------------------------------- /examples/shared/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strconv" 7 | 8 | chai "github.com/go-chai/chai/chi" 9 | "github.com/go-chai/chai/examples/shared/httputil" 10 | "github.com/go-chai/chai/examples/shared/model" 11 | "github.com/go-chi/chi/v5" 12 | ) 13 | 14 | // Controller example 15 | type Controller struct { 16 | } 17 | 18 | // NewController example 19 | func NewController() *Controller { 20 | return &Controller{} 21 | } 22 | 23 | func (c *Controller) ChiRoutes() chi.Router { 24 | r := chi.NewRouter() 25 | 26 | r.Route("/api/v1", func(r chi.Router) { 27 | r.Route("/accounts", func(r chi.Router) { 28 | chai.Get(r, "/{id}", c.ShowAccount) 29 | chai.Get(r, "/", c.ListAccounts) 30 | chai.Post(r, "/", c.AddAccount) 31 | r.Delete("/{id:[0-9]+}", c.DeleteAccount) 32 | r.Patch("/{id}", c.UpdateAccount) 33 | r.Post("/{id}/images", c.UploadAccountImage) 34 | }) 35 | 36 | r.Route("/bottles", func(r chi.Router) { 37 | // ShowBottle godoc 38 | // @Summary Show a bottle 39 | // @Description get string by ID 40 | // @ID get-string-by-int 41 | // @Tags bottles 42 | // @Accept json 43 | // @Produce json 44 | // @Param id path int true "Bottle ID" 45 | // @Success 200 {object} model.Bottle 46 | // @Failure 400 {object} httputil.Error 47 | // @Failure 404 {object} httputil.Error 48 | // @Failure 500 {object} httputil.Error 49 | chai.Get(r, "/{id}", func(w http.ResponseWriter, r *http.Request) (*model.Bottle, int, error) { 50 | id := chi.URLParam(r, "id") 51 | bid, err := strconv.Atoi(id) 52 | if err != nil { 53 | return nil, http.StatusBadRequest, err 54 | } 55 | bottle, err := model.BottleOne(bid) 56 | if err != nil { 57 | return nil, http.StatusNotFound, err 58 | } 59 | return bottle, http.StatusOK, nil 60 | }) 61 | chai.Get(r, "/", c.ListBottles) 62 | }) 63 | 64 | r.Route("/admin", func(r chi.Router) { 65 | r.Use(auth) 66 | 67 | chai.Post(r, "/auth", c.Auth) 68 | }) 69 | 70 | r.Route("/examples", func(r chi.Router) { 71 | chai.Get(r, "/ping", c.PingExample) 72 | chai.Get(r, "/calc", c.CalcExample) 73 | // chai.Get(r, "/group{s/{gro}up_id}/accounts/{account_id}", c.PathParamsExample) 74 | chai.Get(r, "/groups/{group_id}/accounts/{account_id}", c.PathParamsExample) 75 | chai.Get(r, "/header", c.HeaderExample) 76 | chai.Get(r, "/securities", c.SecuritiesExample) 77 | chai.Get(r, "/attribute", c.AttributeExample) 78 | chai.Post(r, "/attribute", c.PostExample) 79 | }) 80 | }) 81 | 82 | return r 83 | } 84 | 85 | func auth(next http.Handler) http.Handler { 86 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 87 | if len(r.Header.Get("Authorization")) == 0 { 88 | httputil.NewError(w, http.StatusUnauthorized, errors.New("Authorization is required Header")) 89 | return 90 | } 91 | next.ServeHTTP(w, r) 92 | }) 93 | } 94 | 95 | // Message example 96 | type Message struct { 97 | Message string `json:"message" example:"message"` 98 | } 99 | 100 | type Message2 struct { 101 | Message string `json:"message" example:"message"` 102 | } 103 | -------------------------------------------------------------------------------- /examples/shared/controller/examples.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/go-chai/chai/examples/shared/httputil" 9 | "github.com/go-chai/chai/examples/shared/model" 10 | "github.com/go-chi/chi/v5" 11 | ) 12 | 13 | // PingExample godoc 14 | // @Summary ping example 15 | // @Description do ping 16 | // @Tags example 17 | // @Accept json 18 | // @Produce json 19 | // @Success 200 {string} string "pong" 20 | // @Failure 400 {string} string "ok" 21 | // @Failure 404 {string} string "ok" 22 | // @Failure 500 {string} string "ok" 23 | func (c *Controller) PingExample(w http.ResponseWriter, r *http.Request) (string, int, error) { 24 | return "pong", http.StatusOK, nil 25 | } 26 | 27 | // CalcExample godoc 28 | // @Summary calc example 29 | // @Description plus 30 | // @Tags example 31 | // @Accept json 32 | // @Produce json 33 | // @Param val1 query int true "used for calc" 34 | // @Param val2 query int true "used for calc" 35 | // @Success 200 {integer} string "answer" 36 | // @Failure 400 {string} string "ok" 37 | // @Failure 404 {string} string "ok" 38 | // @Failure 500 {string} string "ok" 39 | func (c *Controller) CalcExample(w http.ResponseWriter, r *http.Request) (string, int, error) { 40 | val1, err := strconv.Atoi(r.URL.Query().Get("val1")) 41 | if err != nil { 42 | return "", http.StatusBadRequest, err 43 | } 44 | val2, err := strconv.Atoi(r.URL.Query().Get("val2")) 45 | if err != nil { 46 | return "", http.StatusBadRequest, err 47 | } 48 | ans := val1 + val2 49 | return fmt.Sprintf("%d", ans), http.StatusOK, nil 50 | } 51 | 52 | // PathParamsExample godoc 53 | // @Summary path params example 54 | // @Description path params 55 | // @Tags example 56 | // @Accept json 57 | // @Produce json 58 | // @Param group_id path int true "Group ID" 59 | // @Param account_id path int true "Account ID" 60 | // @Success 200 {string} string "answer" 61 | // @Failure 400 {string} string "ok" 62 | // @Failure 404 {string} string "ok" 63 | // @Failure 500 {string} string "ok" 64 | func (c *Controller) PathParamsExample(w http.ResponseWriter, r *http.Request) (string, int, error) { 65 | groupID, err := strconv.Atoi(chi.URLParam(r, "group_id")) 66 | if err != nil { 67 | return "", http.StatusBadRequest, err 68 | } 69 | accountID, err := strconv.Atoi(chi.URLParam(r, "account_id")) 70 | if err != nil { 71 | return "", http.StatusBadRequest, err 72 | } 73 | 74 | return fmt.Sprintf("group_id=%d account_id=%d", groupID, accountID), http.StatusOK, nil 75 | } 76 | 77 | // HeaderExample godoc 78 | // @Summary custome header example 79 | // @Description custome header 80 | // @Tags example 81 | // @Accept json 82 | // @Produce json 83 | // @Param Authorization header string true "Authentication header" 84 | // @Success 200 {string} string "answer" 85 | // @Failure 400 {string} string "ok" 86 | // @Failure 404 {string} string "ok" 87 | // @Failure 500 {string} string "ok" 88 | func (c *Controller) HeaderExample(w http.ResponseWriter, r *http.Request) (string, int, error) { 89 | return r.Header.Get("Authorization"), http.StatusOK, nil 90 | } 91 | 92 | // SecuritiesExample godoc 93 | // @Summary custome header example 94 | // @Description custome header 95 | // @Tags example 96 | // @Accept json 97 | // @Produce json 98 | // @Param Authorization header string true "Authentication header" 99 | // @Success 200 {string} string "answer" 100 | // @Failure 400 {string} string "ok" 101 | // @Failure 404 {string} string "ok" 102 | // @Failure 500 {string} string "ok" 103 | // @Security ApiKeyAuth 104 | // @Security OAuth2Implicit[admin, write] 105 | func (c *Controller) SecuritiesExample(w http.ResponseWriter, r *http.Request) (string, int, error) { 106 | return "ok", http.StatusOK, nil 107 | } 108 | 109 | // AttributeExample godoc 110 | // @Summary attribute example 111 | // @Description attribute 112 | // @Tags example 113 | // @Accept json 114 | // @Produce json 115 | // @Param enumstring query string false "string enums" Enums(A, B, C) 116 | // @Param enumint query int false "int enums" Enums(1, 2, 3) 117 | // @Param enumnumber query number false "int enums" Enums(1.1, 1.2, 1.3) 118 | // @Param string query string false "string valid" minlength(5) maxlength(10) 119 | // @Param int query int false "int valid" minimum(1) maximum(10) 120 | // @Param default query string false "string default" default(A) 121 | // @Success 200 {string} string "answer" 122 | // @Failure 400 {string} string "ok" 123 | // @Failure 404 {string} string "ok" 124 | // @Failure 500 {string} string "ok" 125 | func (c *Controller) AttributeExample(w http.ResponseWriter, r *http.Request) (string, int, error) { 126 | return fmt.Sprintf("enumstring=%s enumint=%s enumnumber=%s string=%s int=%s default=%s", 127 | r.URL.Query().Get("enumstring"), 128 | r.URL.Query().Get("enumint"), 129 | r.URL.Query().Get("enumnumber"), 130 | r.URL.Query().Get("string"), 131 | r.URL.Query().Get("int"), 132 | r.URL.Query().Get("default"), 133 | ), http.StatusOK, nil 134 | } 135 | 136 | // PostExample godoc 137 | // @Summary post request example 138 | // @Description post request example 139 | func (c *Controller) PostExample(account *model.Account, w http.ResponseWriter, r *http.Request) (*model.Account2, int, *httputil.Error) { 140 | return &model.Account2{ 141 | ID: account.ID, 142 | Name: account.Name, 143 | SomeNumber: account.SomeNumber, 144 | UUID: account.UUID, 145 | }, http.StatusOK, nil 146 | } 147 | -------------------------------------------------------------------------------- /examples/shared/httputil/error.go: -------------------------------------------------------------------------------- 1 | package httputil 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | 9 | "github.com/go-chi/render" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // NewError example 14 | func NewError(w http.ResponseWriter, code int, err error) bool { 15 | if err == nil { 16 | return false 17 | } 18 | 19 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 20 | w.Header().Set("X-Content-Type-Options", "nosniff") 21 | w.WriteHeader(code) 22 | 23 | enc := json.NewEncoder(w) 24 | enc.SetEscapeHTML(true) 25 | 26 | err = enc.Encode(Error{StatusCode: code, Message: err.Error()}) 27 | 28 | if err != nil { 29 | panic(err) // If this happens, it's a programmer mistake so we panic 30 | } 31 | 32 | return true 33 | } 34 | 35 | // Error example 36 | type Error struct { 37 | Message string `json:"error"` 38 | ErrorDebug string `json:"error_debug,omitempty"` 39 | ErrorDescription string `json:"error_description,omitempty"` 40 | StatusCode int `json:"status_code,omitempty"` 41 | } 42 | 43 | func (e *Error) Error() string { 44 | return e.Message 45 | } 46 | 47 | func Respond(w http.ResponseWriter, r *http.Request, v interface{}) { 48 | JSON(w, r, v) 49 | } 50 | 51 | func JSON(w http.ResponseWriter, r *http.Request, v interface{}) { 52 | bytes, err := json.Marshal(v) 53 | if err != nil { 54 | http.Error(w, err.Error(), http.StatusInternalServerError) 55 | return 56 | } 57 | 58 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 59 | if status, ok := r.Context().Value(render.StatusCtxKey).(int); ok { 60 | w.WriteHeader(status) 61 | } 62 | w.Write(bytes) 63 | } 64 | 65 | func Decode(r *http.Request, v interface{}) error { 66 | return DecodeJSON(r.Body, v) 67 | } 68 | 69 | func DecodeFormValue(r *http.Request, key string, v interface{}) error { 70 | err := json.Unmarshal([]byte(r.FormValue(key)), v) 71 | if err != nil { 72 | return errors.Wrap(err, "") 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func DecodeJSON(r io.Reader, v interface{}) error { 79 | defer io.Copy(ioutil.Discard, r) 80 | return json.NewDecoder(r).Decode(v) 81 | } 82 | -------------------------------------------------------------------------------- /examples/shared/model/account.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | uuid "github.com/gofrs/uuid" 8 | ) 9 | 10 | type CustomType456 struct { 11 | ZZ int `json:"zz" example:"34"` 12 | ZZStr string `json:"zzStr" example:"zz string value"` 13 | } 14 | 15 | // Account example 16 | type Account2 struct { 17 | ID int `json:"id" example:"113" maximum:"195" format:"int64"` 18 | Name string `json:"name" example:"account name"` 19 | SomeNumber int `json:"some_number" example:"1234"` 20 | CustomVal123 string `json:"custom_val_123" example:"custom val 123"` 21 | CustomType4567 []CustomType456 `json:"custom_type_4567" description:"custom type 4567 description"` // zzz 22 | UUID uuid.UUID `json:"uuid" example:"550e8400-e29b-41d4-a716-446655440000" format:"uuid"` 23 | } 24 | 25 | type Account struct { 26 | ID int `json:"id" example:"1" format:"int64"` 27 | Name string `json:"name" example:"account name"` 28 | SomeNumber int `json:"some_number" example:"1234"` 29 | UUID uuid.UUID `json:"uuid,omitempty" example:"550e8400-e29b-41d4-a716-446655440000" format:"uuid"` 30 | } 31 | 32 | // example 33 | var ( 34 | ErrNameInvalid = errors.New("name is empty") 35 | ) 36 | 37 | // AddAccount example 38 | type AddAccount struct { 39 | Name string `json:"name" example:"account name"` 40 | } 41 | 42 | // Validation example 43 | func (a AddAccount) Validation() error { 44 | switch { 45 | case len(a.Name) == 0: 46 | return ErrNameInvalid 47 | default: 48 | return nil 49 | } 50 | } 51 | 52 | // UpdateAccount example 53 | type UpdateAccount struct { 54 | Name string `json:"name" example:"account name"` 55 | } 56 | 57 | // Validation example 58 | func (a UpdateAccount) Validation() error { 59 | switch { 60 | case len(a.Name) == 0: 61 | return ErrNameInvalid 62 | default: 63 | return nil 64 | } 65 | } 66 | 67 | // AccountsAll example 68 | func AccountsAll(q string) ([]Account, error) { 69 | if q == "" { 70 | return accounts, nil 71 | } 72 | as := []Account{} 73 | for k, v := range accounts { 74 | if q == v.Name { 75 | as = append(as, accounts[k]) 76 | } 77 | } 78 | return as, nil 79 | } 80 | 81 | // AccountOne example 82 | func AccountOne(id int) (Account, error) { 83 | for _, v := range accounts { 84 | if id == v.ID { 85 | return v, nil 86 | } 87 | } 88 | return Account{}, ErrNoRow 89 | } 90 | 91 | // Insert example 92 | func (a Account) Insert() (int, error) { 93 | accountMaxID++ 94 | a.ID = accountMaxID 95 | a.Name = fmt.Sprintf("account_%d", accountMaxID) 96 | accounts = append(accounts, a) 97 | return accountMaxID, nil 98 | } 99 | 100 | // Delete example 101 | func Delete(id int) error { 102 | for k, v := range accounts { 103 | if id == v.ID { 104 | accounts = append(accounts[:k], accounts[k+1:]...) 105 | return nil 106 | } 107 | } 108 | return fmt.Errorf("account id=%d is not found", id) 109 | } 110 | 111 | // Update example 112 | func (a Account) Update() error { 113 | for k, v := range accounts { 114 | if a.ID == v.ID { 115 | accounts[k].Name = a.Name 116 | return nil 117 | } 118 | } 119 | return fmt.Errorf("account id=%d is not found", a.ID) 120 | } 121 | 122 | var accountMaxID = 3 123 | var accounts = []Account{ 124 | {ID: 1, Name: "account_1", UUID: uuid.Must(uuid.NewV4())}, 125 | {ID: 2, Name: "account_2", UUID: uuid.Must(uuid.NewV4())}, 126 | {ID: 3, Name: "account_3", UUID: uuid.Must(uuid.NewV4())}, 127 | } 128 | -------------------------------------------------------------------------------- /examples/shared/model/admin.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Admin example 4 | type Admin struct { 5 | ID int `json:"id" example:"1"` 6 | Name string `json:"name" example:"admin name"` 7 | } 8 | 9 | // Message example 10 | type Message struct { 11 | Message string `json:"message" example:"message"` 12 | } 13 | -------------------------------------------------------------------------------- /examples/shared/model/bottle.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Bottle example 4 | type Bottle struct { 5 | ID int `json:"id" example:"1"` 6 | Name string `json:"name" example:"bottle_name"` 7 | Account Account `json:"account"` 8 | } 9 | 10 | // BottlesAll example 11 | func BottlesAll() ([]Bottle, error) { 12 | return bottles, nil 13 | } 14 | 15 | // BottleOne example 16 | func BottleOne(id int) (*Bottle, error) { 17 | for _, v := range bottles { 18 | if id == v.ID { 19 | return &v, nil 20 | } 21 | } 22 | return nil, ErrNoRow 23 | } 24 | 25 | var bottles = []Bottle{ 26 | {ID: 1, Name: "bottle_1", Account: Account{ID: 1, Name: "accout_1"}}, 27 | {ID: 2, Name: "bottle_2", Account: Account{ID: 2, Name: "accout_2"}}, 28 | {ID: 3, Name: "bottle_3", Account: Account{ID: 3, Name: "accout_3"}}, 29 | } 30 | -------------------------------------------------------------------------------- /examples/shared/model/error.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrNoRow example 7 | ErrNoRow = errors.New("no rows in result set") 8 | ) 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-chai/chai 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/getkin/kin-openapi v0.88.0 7 | github.com/ghodss/yaml v1.0.0 8 | github.com/go-chai/swag v1.7.8-fork2 9 | github.com/go-chi/chi/v5 v5.0.7 10 | github.com/go-chi/render v1.0.1 11 | github.com/go-openapi/spec v0.20.4 12 | github.com/gofrs/uuid v4.2.0+incompatible 13 | github.com/gorilla/mux v1.8.0 14 | github.com/pkg/errors v0.9.1 15 | github.com/stretchr/testify v1.7.0 16 | github.com/swaggo/http-swagger v1.2.6 17 | github.com/swaggo/swag v1.7.9 18 | ) 19 | 20 | require ( 21 | github.com/KyleBanks/depth v1.2.1 // indirect 22 | github.com/PuerkitoBio/purell v1.1.1 // indirect 23 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 26 | github.com/go-openapi/jsonreference v0.19.6 // indirect 27 | github.com/go-openapi/swag v0.19.15 // indirect 28 | github.com/josharian/intern v1.0.0 // indirect 29 | github.com/mailru/easyjson v0.7.6 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 // indirect 32 | golang.org/x/net v0.7.0 // indirect 33 | golang.org/x/sys v0.5.0 // indirect 34 | golang.org/x/text v0.7.0 // indirect 35 | golang.org/x/tools v0.1.12 // indirect 36 | gopkg.in/yaml.v2 v2.4.0 // indirect 37 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 3 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 4 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 5 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 6 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 7 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 8 | github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs= 9 | github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= 10 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 11 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/getkin/kin-openapi v0.88.0 h1:BjJ2JERWJbYE1o1RGEj/5LmR5qw7ecfl3O3su4ImR+0= 16 | github.com/getkin/kin-openapi v0.88.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= 17 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 18 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 19 | github.com/go-chai/swag v1.7.8-fork2 h1:ijgxBjnbrjRK1XAqLor+FvMLpdST7MEk2MGw3V+Zz0I= 20 | github.com/go-chai/swag v1.7.8-fork2/go.mod h1:7osfZ2yWVpvxjKkjueEGIi5GIltebBc7YsfN8JtRURc= 21 | github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= 22 | github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 23 | github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= 24 | github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= 25 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 26 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 27 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 28 | github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= 29 | github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= 30 | github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= 31 | github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= 32 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 33 | github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= 34 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 35 | github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= 36 | github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 37 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 38 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 39 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 40 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 41 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 42 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 43 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 44 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 45 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 46 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 47 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 48 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 49 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 50 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 51 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 52 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 53 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 54 | github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE= 55 | github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= 56 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= 57 | github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= 58 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= 59 | github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= 60 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 61 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 62 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 63 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 64 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 65 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 66 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 67 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 68 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 69 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 70 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 71 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 72 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 73 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 74 | github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 h1:+iNTcqQJy0OZ5jk6a5NLib47eqXK8uYcPX+O4+cBpEM= 75 | github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= 76 | github.com/swaggo/http-swagger v1.2.6 h1:ihTjChUoSRMpFMjWw+0AkL1Ti4r6v8pCgVYLmQVRlRw= 77 | github.com/swaggo/http-swagger v1.2.6/go.mod h1:CcoICgY3yVDk2u1LQUCMHbAj0fjlxIX+873psXlIKNA= 78 | github.com/swaggo/swag v1.7.9 h1:6vCG5mm43ebDzGlZPMGYrYI4zKFfOr5kicQX8qjeDwc= 79 | github.com/swaggo/swag v1.7.9/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU= 80 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 81 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 82 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 83 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 84 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 85 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= 86 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 87 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 88 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 89 | golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 90 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 91 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 92 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 93 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 94 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 96 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 102 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 103 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 104 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 105 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 106 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 107 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= 108 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 109 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 110 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 111 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 112 | golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 113 | golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= 114 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 115 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 116 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 117 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 118 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 119 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 120 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 121 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 122 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 123 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 124 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 125 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 126 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 127 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 128 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 129 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 130 | -------------------------------------------------------------------------------- /gorilla/methods.go: -------------------------------------------------------------------------------- 1 | package chai 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chai/chai/chai" 7 | "github.com/gorilla/mux" 8 | ) 9 | 10 | func Get[Res any, Err chai.ErrType](r *mux.Router, path string, fn chai.ResHandlerFunc[Res, Err]) { 11 | r.Methods(http.MethodGet).Path(path).Handler(chai.NewResHandler(fn)) 12 | } 13 | 14 | func Connect[Res any, Err chai.ErrType](r *mux.Router, path string, fn chai.ResHandlerFunc[Res, Err]) { 15 | r.Methods(http.MethodConnect).Path(path).Handler(chai.NewResHandler(fn)) 16 | } 17 | 18 | func Options[Res any, Err chai.ErrType](r *mux.Router, path string, fn chai.ResHandlerFunc[Res, Err]) { 19 | r.Methods(http.MethodOptions).Path(path).Handler(chai.NewResHandler(fn)) 20 | } 21 | 22 | func Post[Req any, Res any, Err chai.ErrType](r *mux.Router, path string, fn chai.ReqResHandlerFunc[Req, Res, Err]) { 23 | r.Methods(http.MethodPost).Path(path).Handler(chai.NewReqResHandler(fn)) 24 | } 25 | func Put[Req any, Res any, Err chai.ErrType](r *mux.Router, path string, fn chai.ReqResHandlerFunc[Req, Res, Err]) { 26 | r.Methods(http.MethodPut).Path(path).Handler(chai.NewReqResHandler(fn)) 27 | } 28 | 29 | func Patch[Req any, Res any, Err chai.ErrType](r *mux.Router, path string, fn chai.ReqResHandlerFunc[Req, Res, Err]) { 30 | r.Methods(http.MethodPatch).Path(path).Handler(chai.NewReqResHandler(fn)) 31 | } 32 | 33 | func Delete[Req any, Res any, Err chai.ErrType](r *mux.Router, path string, fn chai.ReqResHandlerFunc[Req, Res, Err]) { 34 | r.Methods(http.MethodDelete).Path(path).Handler(chai.NewReqResHandler(fn)) 35 | } 36 | -------------------------------------------------------------------------------- /gorilla/openapi2.go: -------------------------------------------------------------------------------- 1 | package chai 2 | 3 | import ( 4 | "strings" 5 | 6 | chai "github.com/go-chai/chai/chi" 7 | "github.com/go-chai/chai/openapi2" 8 | "github.com/go-openapi/spec" 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | func OpenAPI2(r *mux.Router) (*spec.Swagger, error) { 13 | routes, err := getGorillaRoutes(r) 14 | 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return openapi2.Docs(routes) 20 | } 21 | 22 | func getGorillaRoutes(r *mux.Router) ([]*openapi2.Route, error) { 23 | routes := make([]*openapi2.Route, 0) 24 | 25 | err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { 26 | path, err := route.GetPathTemplate() 27 | if err != nil { 28 | return err 29 | } 30 | methods, err := route.GetMethods() 31 | if err != nil && !strings.Contains(err.Error(), "route doesn't have methods") { 32 | return err 33 | } 34 | 35 | for _, method := range methods { 36 | params, regexlessPath := chai.ParsePathParams(path) 37 | 38 | routes = append(routes, &openapi2.Route{ 39 | Method: method, 40 | Path: regexlessPath, 41 | Params: params, 42 | Handler: route.GetHandler(), 43 | }) 44 | } 45 | 46 | return nil 47 | }) 48 | 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return routes, nil 54 | } 55 | -------------------------------------------------------------------------------- /gorilla/openapi3.go: -------------------------------------------------------------------------------- 1 | package chai 2 | 3 | import ( 4 | kinopenapi3 "github.com/getkin/kin-openapi/openapi3" 5 | "github.com/go-chai/chai/openapi3" 6 | "github.com/gorilla/mux" 7 | ) 8 | 9 | func OpenAPI3(r *mux.Router) (*kinopenapi3.T, error) { 10 | routes, err := getGorillaRoutes(r) 11 | if err != nil { 12 | return nil, err 13 | } 14 | return openapi3.Docs(routes) 15 | } 16 | -------------------------------------------------------------------------------- /internal/tests/testfile.go: -------------------------------------------------------------------------------- 1 | // IT IS IMPORTANT TO KEEP THE FORMATTING OF THIS FILE EXACTLY AS IT IS. 2 | 3 | package tests 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/go-chai/chai/chai" 9 | ) 10 | 11 | //Simple correct comment 12 | func Simple(string, 13 | string) chai.ResHandlerFunc[string, error] { 14 | 15 | // 16 | 17 | // 18 | 19 | // 20 | 21 | // 22 | 23 | // Simple wrong comment 24 | return func(w http.ResponseWriter, r *http.Request) (string, int, error) { return "", 3, nil }; return nil; return nil 25 | 26 | // 27 | 28 | // 29 | } 30 | 31 | //Simple2 correct comment 32 | func Simple2(string, 33 | string) chai.ResHandlerFunc[string, error] { 34 | 35 | // 36 | 37 | // 38 | 39 | // 40 | 41 | // 42 | 43 | // Simple2 wrong comment 44 | return func(w http.ResponseWriter, r *http.Request) (string, int, error) { return "", 3, nil }; return nil; return nil 45 | 46 | // 47 | 48 | // 49 | } 50 | 51 | //NotSimple correct comment 52 | func NotSimple(string, 53 | string) chai.ResHandlerFunc[string, error] { 54 | 55 | // 56 | 57 | // 58 | 59 | // 60 | 61 | // 62 | 63 | x := 3 64 | 65 | //NotSimple wrong comment 66 | return func(w http.ResponseWriter, r *http.Request) (string, int, error) { return "", x, nil }; return nil; return nil 67 | 68 | // 69 | 70 | // 71 | } 72 | 73 | //NotSimple2 correct comment 74 | func NotSimple2(string, 75 | string) chai.ResHandlerFunc[string, error] { 76 | 77 | // 78 | 79 | // 80 | 81 | // 82 | 83 | // 84 | 85 | x := 3 86 | 87 | //NotSimple2 wrong comment 88 | return func(w http.ResponseWriter, r *http.Request) (string, int, error) { return "", x, nil }; return nil; return nil 89 | 90 | // 91 | 92 | // 93 | } 94 | 95 | //Simple3 correct comment 96 | func Simple3(string, 97 | 98 | 99 | 100 | 101 | string) chai.ResHandlerFunc[string, error] { 102 | 103 | // 104 | 105 | // 106 | // 107 | 108 | // 109 | 110 | // 111 | 112 | // Simple3 wrong comment 113 | 114 | if false {return nil} 115 | 116 | { return nil} 117 | 118 | return nil; { return func(w http.ResponseWriter, r *http.Request) (string, int, error) { return "", 3, nil }; return nil; return nil} 119 | // 120 | 121 | // 122 | } 123 | 124 | // Simple4 outer comment 125 | func Simple4() func() (int) { 126 | 127 | 128 | // 129 | 130 | 131 | // 132 | 133 | 134 | // Simple4 inner comment 135 | return func() (int) { return 1} 136 | } -------------------------------------------------------------------------------- /internal/tests/testtypes.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | type TestStruct struct { 4 | Foo string `json:"foo"` 5 | Bar int `json:"bar"` 6 | } 7 | 8 | type TestInnerResponse struct { 9 | FooFoo int `json:"foo_foo"` 10 | BarBar int `json:"bar_bar"` 11 | } 12 | type TestResponse struct { 13 | Foo string `json:"foo"` 14 | Bar string `json:"bar"` 15 | 16 | TestInnerResponse TestInnerResponse `json:"test_inner_response"` 17 | } 18 | 19 | type TestRequest struct { 20 | Foo string `json:"foob"` 21 | Bar string `json:"barb"` 22 | 23 | TestInnerResponse TestInnerResponse `json:"test_inner_responseb"` 24 | } 25 | 26 | type TestError struct { 27 | Message string `json:"message"` 28 | } 29 | 30 | func (e TestError) Error() string { 31 | return e.Message 32 | } 33 | 34 | type TestErrorPtr struct { 35 | Message string `json:"message"` 36 | } 37 | 38 | func (e *TestErrorPtr) Error() string { 39 | return e.Message 40 | } 41 | 42 | type TestErrorMap map[string]string 43 | 44 | func (e TestErrorMap) Error() string { 45 | return "test error map" 46 | } 47 | 48 | type TestErrorMapPtr map[string]string 49 | 50 | func (e *TestErrorMapPtr) Error() string { 51 | return "test error map ptr" 52 | } 53 | -------------------------------------------------------------------------------- /internal/tests/util.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "encoding/json" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func LoadFile(t *testing.T, path string) string { 13 | b, err := ioutil.ReadFile(path) 14 | require.NoError(t, err) 15 | return string(b) 16 | } 17 | 18 | func JS(v any) string { 19 | b, _ := json.MarshalIndent(v, "", " ") 20 | return string(b) 21 | } 22 | -------------------------------------------------------------------------------- /internal/tests/xassert/assert.go: -------------------------------------------------------------------------------- 1 | package xassert 2 | 3 | import ( 4 | "fmt" 5 | 6 | "encoding/json" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // JSONEq asserts that two JSON strings are equivalent. 12 | // 13 | // assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) 14 | func JSONEq(t assert.TestingT, expected string, actual string, msgAndArgs ...interface{}) bool { 15 | if h, ok := t.(tHelper); ok { 16 | h.Helper() 17 | } 18 | var expectedJSONAsInterface, actualJSONAsInterface interface{} 19 | 20 | if err := json.Unmarshal([]byte(expected), &expectedJSONAsInterface); err != nil { 21 | return assert.Fail(t, fmt.Sprintf("Expected value ('%s') is not valid json.\nJSON parsing error: '%s'", expected, err.Error()), msgAndArgs...) 22 | } 23 | 24 | if err := json.Unmarshal([]byte(actual), &actualJSONAsInterface); err != nil { 25 | return assert.Fail(t, fmt.Sprintf("Input ('%s') needs to be valid json.\nJSON parsing error: '%s'", actual, err.Error()), msgAndArgs...) 26 | } 27 | 28 | return assert.Equal(t, expectedJSONAsInterface, actualJSONAsInterface, msgAndArgs...) 29 | } 30 | 31 | type tHelper interface { 32 | Helper() 33 | } 34 | -------------------------------------------------------------------------------- /internal/tests/xrequire/require.go: -------------------------------------------------------------------------------- 1 | package xrequire 2 | 3 | import ( 4 | "github.com/go-chai/chai/internal/tests/xassert" 5 | "github.com/stretchr/testify/require" 6 | ) 7 | 8 | // JSONEq asserts that two JSON strings are equivalent. 9 | // 10 | // assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) 11 | func JSONEq(t require.TestingT, expected string, actual string, msgAndArgs ...interface{}) { 12 | if h, ok := t.(tHelper); ok { 13 | h.Helper() 14 | } 15 | if xassert.JSONEq(t, expected, actual, msgAndArgs...) { 16 | return 17 | } 18 | t.FailNow() 19 | } 20 | 21 | type tHelper interface { 22 | Helper() 23 | } 24 | -------------------------------------------------------------------------------- /openapi2/funcinfo.go: -------------------------------------------------------------------------------- 1 | package openapi2 2 | 3 | import ( 4 | "go/ast" 5 | "go/parser" 6 | "go/token" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "runtime" 11 | "strings" 12 | ) 13 | 14 | type funcInfo struct { 15 | Pkg string `json:"pkg"` 16 | Func string `json:"func"` 17 | Comment string `json:"comment"` 18 | File string `json:"file,omitempty"` 19 | ASTFile *ast.File `json:"ast_file,omitempty"` 20 | FSet *token.FileSet `json:"fset,omitempty"` 21 | Line int `json:"line,omitempty"` 22 | Anonymous bool `json:"anonymous,omitempty"` 23 | Unresolvable bool `json:"unresolvable,omitempty"` 24 | } 25 | 26 | func getFuncInfo(fn any) funcInfo { 27 | return getFuncInfoWithSrc(fn, nil) 28 | } 29 | 30 | func getFuncInfoWithSrc(fn any, src any) funcInfo { 31 | fi := funcInfo{} 32 | frame := getCallerFrame(fn) 33 | goPathSrc := filepath.Join(os.Getenv("GOPATH"), "src") 34 | 35 | if frame == nil { 36 | fi.Unresolvable = true 37 | return fi 38 | } 39 | 40 | pkgName := getPkgName(frame.File, src) 41 | funcPath := frame.Func.Name() 42 | 43 | idx := strings.Index(funcPath, "/"+pkgName) 44 | if idx > 0 { 45 | fi.Pkg = funcPath[:idx+1+len(pkgName)] 46 | fi.Func = funcPath[idx+2+len(pkgName):] 47 | } else { 48 | fi.Func = funcPath 49 | } 50 | 51 | if strings.Index(fi.Func, ".func") > 0 { 52 | fi.Anonymous = true 53 | } 54 | 55 | fi.File = frame.File 56 | fi.Line = frame.Line 57 | if filepath.HasPrefix(fi.File, goPathSrc) { 58 | fi.File = fi.File[len(goPathSrc)+1:] 59 | } 60 | 61 | if !fi.Unresolvable { 62 | fi.Comment, fi.ASTFile, fi.FSet = getFuncComment(frame.File, frame.Line, src) 63 | } 64 | return fi 65 | } 66 | 67 | func getCallerFrame(fn any) *runtime.Frame { 68 | pc := reflect.ValueOf(fn).Pointer() 69 | frames := runtime.CallersFrames([]uintptr{pc}) 70 | if frames == nil { 71 | return nil 72 | } 73 | frame, _ := frames.Next() 74 | if frame.Entry == 0 { 75 | return nil 76 | } 77 | return &frame 78 | } 79 | 80 | func getPkgName(file string, src any) string { 81 | fset := token.NewFileSet() 82 | astFile, err := parser.ParseFile(fset, file, src, parser.PackageClauseOnly) 83 | if err != nil { 84 | return "" 85 | } 86 | if astFile.Name == nil { 87 | return "" 88 | } 89 | return astFile.Name.Name 90 | } 91 | 92 | func getFuncComment(file string, line int, src any) (string, *ast.File, *token.FileSet) { 93 | fset := token.NewFileSet() 94 | 95 | astFile, err := parser.ParseFile(fset, file, src, parser.ParseComments) 96 | if err != nil { 97 | return "", nil, nil 98 | } 99 | 100 | if len(astFile.Comments) == 0 { 101 | return "", astFile, fset 102 | } 103 | 104 | line = fixFuncLine(line, fset, astFile) 105 | for _, cmt := range astFile.Comments { 106 | if fset.Position(cmt.End()).Line+1 == line { 107 | return cmt.Text(), astFile, fset 108 | } 109 | } 110 | 111 | return "", astFile, fset 112 | } 113 | 114 | func pos(fset *token.FileSet, n ast.Node) int { 115 | return fset.Position(n.Pos()).Line 116 | } 117 | 118 | // If the compiler inlined the function, we get the line of the return statement rather than the line of the function definition. 119 | // fixFuncLine checks if the specified line contains any return statements 120 | // and if so, returns the line of the function definition that the first return belongs to. 121 | func fixFuncLine(line int, fset *token.FileSet, astFile *ast.File) int { 122 | fixedFuncLine := line 123 | 124 | var stack []ast.Node 125 | ast.Inspect(astFile, func(n ast.Node) bool { 126 | if n != nil { 127 | stack = append(stack, n) 128 | } else { 129 | stack = stack[:len(stack)-1] 130 | } 131 | 132 | // Check if the current node is on the specified line. 133 | if n == nil || fset.Position(n.Pos()).Line != line { 134 | return true 135 | } 136 | // Check if the current node is a return statement. 137 | _, ok := n.(*ast.ReturnStmt) 138 | if !ok { 139 | return true 140 | } 141 | // Starting at the return statement, go up the node stack until we find the first function definition 142 | for i := len(stack) - 1; i >= 0; i-- { 143 | switch parent := stack[i].(type) { 144 | case *ast.FuncDecl, *ast.FuncLit: 145 | fixedFuncLine = pos(fset, parent) 146 | 147 | // Stop looking after the function definition of the first return statement 148 | return false 149 | } 150 | } 151 | 152 | return true 153 | }) 154 | 155 | return fixedFuncLine 156 | } 157 | -------------------------------------------------------------------------------- /openapi2/funcinfo_test.go: -------------------------------------------------------------------------------- 1 | // wrong comment 1 2 | 3 | package openapi2 4 | 5 | import ( 6 | "go/parser" 7 | "go/token" 8 | "testing" 9 | 10 | "github.com/go-chai/chai/internal/tests" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestFixFuncLine(t *testing.T) { 15 | type args struct { 16 | filePath string 17 | line int 18 | } 19 | type want struct { 20 | fixedLine int 21 | } 22 | tests := []struct { 23 | name string 24 | args args 25 | want want 26 | }{ 27 | { 28 | name: "simple", 29 | args: args{ 30 | filePath: "../internal/tests/testfile.go", 31 | line: 24, 32 | }, 33 | want: want{fixedLine: 12}, 34 | }, 35 | { 36 | name: "simple2", 37 | args: args{ 38 | filePath: "../internal/tests/testfile.go", 39 | line: 44, 40 | }, 41 | want: want{fixedLine: 32}, 42 | }, 43 | { 44 | name: "not simple", 45 | args: args{ 46 | filePath: "../internal/tests/testfile.go", 47 | line: 52, 48 | }, 49 | want: want{fixedLine: 52}, 50 | }, 51 | { 52 | name: "not simple 2", 53 | args: args{ 54 | filePath: "../internal/tests/testfile.go", 55 | line: 74, 56 | }, 57 | want: want{fixedLine: 74}, 58 | }, 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | fset := token.NewFileSet() 63 | astFile, err := parser.ParseFile(fset, tt.args.filePath, nil, parser.ParseComments) 64 | require.NoError(t, err) 65 | 66 | fixedLine := fixFuncLine(tt.args.line, fset, astFile) 67 | require.Equal(t, tt.want.fixedLine, fixedLine) 68 | }) 69 | } 70 | } 71 | 72 | func TestGetFuncInfo(t *testing.T) { 73 | type args struct { 74 | fn any 75 | } 76 | type want struct { 77 | comment string 78 | } 79 | tests := []struct { 80 | name string 81 | args args 82 | want want 83 | }{ 84 | { 85 | name: "simple", 86 | args: args{ 87 | fn: tests.Simple, 88 | }, 89 | want: want{comment: "Simple correct comment\n"}, 90 | }, 91 | { 92 | name: "not simple", 93 | args: args{ 94 | fn: tests.NotSimple, 95 | }, 96 | want: want{comment: "NotSimple correct comment\n"}, 97 | }, 98 | { 99 | name: "simple2", 100 | args: args{ 101 | fn: tests.Simple2, 102 | }, 103 | want: want{comment: "Simple2 correct comment\n"}, 104 | }, 105 | { 106 | name: "not simple2", 107 | args: args{ 108 | fn: tests.NotSimple2, 109 | }, 110 | want: want{comment: "NotSimple2 correct comment\n"}, 111 | }, 112 | { 113 | name: "simple3", 114 | args: args{ 115 | fn: tests.Simple3, 116 | }, 117 | want: want{comment: "Simple3 correct comment\n"}, 118 | }, 119 | { 120 | name: "simple4 outer", 121 | args: args{ 122 | fn: tests.Simple4, 123 | }, 124 | want: want{comment: "Simple4 outer comment\n"}, 125 | }, 126 | { 127 | name: "simple4 inner", 128 | args: args{ 129 | fn: tests.Simple4(), 130 | }, 131 | want: want{comment: "Simple4 outer comment\n"}, 132 | }, 133 | } 134 | for _, tt := range tests { 135 | t.Run(tt.name, func(t *testing.T) { 136 | require.Equal(t, tt.want.comment, getFuncInfo(tt.args.fn).Comment) 137 | }) 138 | } 139 | } 140 | 141 | // wrong comment 2 142 | func TestGetFuncInfoLocal(t *testing.T) { 143 | 144 | // wrong comment 3 145 | var fn3 func() (int, int) 146 | 147 | //fn comment 148 | fn := func(a, b int) (int, int) { 149 | 150 | // 151 | 152 | return a, b 153 | } 154 | 155 | // fn2 comment 156 | fn2 := func() (int, int) { 157 | 158 | // fn3 comment 159 | fn3 = func() (int, int) { 160 | return 1, 2 161 | } 162 | 163 | return 3, 4 164 | } 165 | 166 | type args struct { 167 | fn any 168 | } 169 | type want struct { 170 | comment string 171 | unresolvable bool 172 | } 173 | tests := []struct { 174 | name string 175 | args args 176 | want want 177 | }{ 178 | { 179 | name: "fn", 180 | args: args{ 181 | fn: fn, 182 | }, 183 | want: want{comment: "fn comment\n", unresolvable: false}, 184 | }, 185 | { 186 | name: "fn2", 187 | args: args{ 188 | fn: fn2, 189 | }, 190 | want: want{comment: "fn2 comment\n", unresolvable: false}, 191 | }, 192 | { 193 | name: "fn3", 194 | args: args{ 195 | fn: fn3, 196 | }, 197 | want: want{comment: "fn3 comment\n", unresolvable: true}, 198 | }, 199 | } 200 | for _, tt := range tests { 201 | t.Run(tt.name, func(t *testing.T) { 202 | fi := getFuncInfo(tt.args.fn) 203 | require.Equal(t, tt.want.unresolvable, fi.Unresolvable) 204 | 205 | if !tt.want.unresolvable { 206 | require.Equal(t, tt.want.comment, fi.Comment) 207 | } 208 | }) 209 | } 210 | } 211 | 212 | // wrong comment 2 213 | func TestGetFuncInfo3(t *testing.T) { 214 | tt(t) 215 | } 216 | -------------------------------------------------------------------------------- /openapi2/funcinfo_zz_test.go: -------------------------------------------------------------------------------- 1 | // wrong comment 1 2 | 3 | package openapi2 4 | 5 | import ( 6 | "testing" 7 | 8 | // "github.com/go-chai/chai/chai" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | //tt - wrong comment 1 14 | func tt(t *testing.T) (int, int) { 15 | 16 | // 17 | 18 | // 19 | 20 | //tt - wrong comment 2 21 | if false { 22 | 23 | return 1, 2 24 | } 25 | //tt - correct comment 26 | fn := func() (int, int) { return 3, 4 }; fn2 := func() (int, int) { return 3, 4 }; fn3 := func() (int, int) { return 1, 2 } 27 | 28 | type args struct { 29 | fn any 30 | } 31 | type want struct { 32 | comment string 33 | unresolvable bool 34 | } 35 | tests := []struct { 36 | name string 37 | args args 38 | want want 39 | }{ 40 | { 41 | name: "fn", 42 | args: args{ 43 | fn: fn, 44 | }, 45 | want: want{comment: "tt - correct comment\n", unresolvable: false}, 46 | }, 47 | { 48 | name: "fn2", 49 | args: args{ 50 | fn: fn2, 51 | }, 52 | want: want{comment: "tt - correct comment\n", unresolvable: false}, 53 | }, 54 | { 55 | name: "fn3", 56 | args: args{ 57 | fn: fn3, 58 | }, 59 | want: want{comment: "tt - correct comment\n", unresolvable: false}, 60 | }, 61 | } 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | // fi := docgen.GetFuncInfo(tt.args.fn) 65 | fi := getFuncInfo(tt.args.fn) 66 | require.Equal(t, tt.want.unresolvable, fi.Unresolvable) 67 | 68 | if !tt.want.unresolvable { 69 | require.Equal(t, tt.want.comment, fi.Comment) 70 | } 71 | }) 72 | } 73 | 74 | return 3, 3 75 | } 76 | -------------------------------------------------------------------------------- /openapi2/log.go: -------------------------------------------------------------------------------- 1 | package openapi2 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func LogYAML(v any) { 10 | bytes, err := MarshalYAML(v) 11 | if err != nil { 12 | panic(err) 13 | } 14 | 15 | fmt.Fprintln(os.Stderr, string(bytes)) 16 | 17 | return 18 | } 19 | 20 | func LogJSON(v any) { 21 | bytes, err := json.MarshalIndent(v, "", " ") 22 | 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | fmt.Println(string(bytes)) 28 | 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /openapi2/marshal.go: -------------------------------------------------------------------------------- 1 | package openapi2 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/ghodss/yaml" 7 | ) 8 | 9 | func MarshalYAML(v interface{}) ([]byte, error) { 10 | return yaml.Marshal(v) 11 | } 12 | 13 | func MarshalJSON(v interface{}) ([]byte, error) { 14 | return json.Marshal(v) 15 | } 16 | -------------------------------------------------------------------------------- /openapi2/openapi2.go: -------------------------------------------------------------------------------- 1 | package openapi2 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/go-chai/chai/chai" 13 | "github.com/go-chai/swag" 14 | "github.com/go-chai/swag/gen" 15 | "github.com/go-openapi/spec" 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | type GenConfig = gen.GenConfig 20 | 21 | func WriteDocs(docs *spec.Swagger, cfg *GenConfig) error { 22 | return gen.New().Generate(docs, cfg) 23 | } 24 | 25 | type Route struct { 26 | Method string 27 | Path string 28 | Params []spec.Parameter 29 | Handler http.Handler 30 | Middlewares []func(http.Handler) http.Handler 31 | } 32 | 33 | func Docs(routes []*Route) (*spec.Swagger, error) { 34 | var err error 35 | 36 | parser := swag.New(swag.SetDebugger(log.Default()), func(p *swag.Parser) { 37 | p.ParseDependency = true 38 | }) 39 | 40 | for _, route := range routes { 41 | err = RegisterRoute(parser, route) 42 | if err != nil { 43 | return nil, err 44 | } 45 | } 46 | 47 | return parser.GetSwagger(), nil 48 | } 49 | 50 | func RegisterRoute(parser *swag.Parser, route *Route) error { 51 | var h = route.Handler 52 | var hh any = h 53 | 54 | ch, ok := h.(chai.Handlerer) 55 | if ok { 56 | hh = ch.Handler() 57 | } 58 | 59 | fi := getFuncInfo(hh) 60 | 61 | if fi.Unresolvable { 62 | return errors.New("failed to resolve func info") 63 | } 64 | 65 | op, err := parseSwaggoAnnotations(fi, parser) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | err = updateRequests(fi, op, h, route.Params) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | err = updateResponses(fi, op, h) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | addOperation(parser.GetSwagger(), route.Path, route.Method, op) 81 | 82 | return nil 83 | } 84 | 85 | func parseSwaggoAnnotations(fi funcInfo, parser *swag.Parser) (*swag.Operation, error) { 86 | var err error 87 | op := swag.NewOperation(parser) 88 | 89 | pkg, err := getPkgPath(fi.File) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | err = parser.GetAllGoFileInfoAndParseTypes(pkg) 95 | if err != nil { 96 | return nil, errors.Wrap(err, "failed to parse docs spec") 97 | } 98 | 99 | for _, line := range strings.Split(fi.Comment, "\n") { 100 | err := op.ParseComment(line, fi.ASTFile) 101 | if err != nil { 102 | return nil, errors.Wrap(err, "failed to parse comment") 103 | } 104 | } 105 | 106 | return op, nil 107 | } 108 | 109 | func getPkgPath(file string) (string, error) { 110 | wd, err := os.Getwd() 111 | if err != nil { 112 | return "", errors.Wrap(err, "failed to get working directory") 113 | } 114 | 115 | file, err = filepath.Rel(wd, file) 116 | if err != nil { 117 | return "", errors.Wrap(err, "failed to get relative path") 118 | } 119 | 120 | return filepath.Dir(file), nil 121 | } 122 | 123 | func updateRequests(fi funcInfo, op *swag.Operation, h http.Handler, params []spec.Parameter) error { 124 | var err error 125 | 126 | reqer, ok := h.(chai.Reqer) 127 | if !ok { 128 | op.Parameters = mergeParameters(params, op.Parameters) 129 | 130 | return nil 131 | } 132 | 133 | if len(op.Consumes) == 0 { 134 | op.Consumes = append(op.Consumes, "application/json") 135 | } 136 | 137 | schema, err := op.ParseAPIObjectSchema("object", typeName(reqer.Req()), fi.ASTFile) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | noBody := true 143 | for i := range op.Parameters { 144 | if op.Parameters[i].In == "body" { 145 | noBody = false 146 | if op.Parameters[i].Schema == nil { 147 | op.Parameters[i].Schema = schema 148 | } 149 | } 150 | } 151 | if noBody { 152 | op.AddParam(spec.BodyParam("body", schema)) 153 | } 154 | 155 | op.Parameters = mergeParameters(params, op.Parameters) 156 | 157 | return nil 158 | } 159 | 160 | type pk struct { 161 | In string 162 | Name string 163 | } 164 | 165 | func less(pk, pk2 pk) bool { 166 | if pk.In == pk2.In { 167 | return pk.Name < pk2.Name 168 | } 169 | 170 | return pk.In < pk2.In 171 | } 172 | 173 | func mergeParameters(paramsList ...[]spec.Parameter) []spec.Parameter { 174 | m := make(map[pk]spec.Parameter) 175 | 176 | for _, params := range paramsList { 177 | m = mergeMaps(m, associateBy(params, func(p spec.Parameter) pk { 178 | return pk{p.In, p.Name} 179 | })) 180 | } 181 | 182 | return sortedValues(m, less) 183 | } 184 | 185 | func mergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { 186 | res := make(map[K]V) 187 | 188 | for _, m := range maps { 189 | for k, v := range m { 190 | res[k] = v 191 | } 192 | } 193 | 194 | return res 195 | } 196 | 197 | func associateBy[K comparable, V any](slice []V, keyFn func(V) K) map[K]V { 198 | m := make(map[K]V) 199 | 200 | for _, t := range slice { 201 | m[keyFn(t)] = t 202 | } 203 | 204 | return m 205 | } 206 | 207 | func sortedValues[K comparable, V any](m map[K]V, less func(K, K) bool) []V { 208 | res := make([]V, len(m)) 209 | 210 | for i, k := range sortedKeys(m, less) { 211 | res[i] = m[k] 212 | } 213 | 214 | return res 215 | } 216 | 217 | func sortedKeys[K comparable, V any](m map[K]V, less func(K, K) bool) []K { 218 | keys := make([]K, len(m)) 219 | i := 0 220 | for k := range m { 221 | keys[i] = k 222 | i++ 223 | } 224 | 225 | if less == nil { 226 | return keys 227 | } 228 | 229 | sort.Slice(keys, func(i, j int) bool { return less(keys[i], keys[j]) }) 230 | 231 | return keys 232 | } 233 | 234 | func updateResponses(fi funcInfo, op *swag.Operation, h http.Handler) error { 235 | resErrer, ok := h.(chai.ResErrer) 236 | if !ok { 237 | return nil 238 | } 239 | 240 | if len(op.Produces) == 0 { 241 | op.Produces = append(op.Produces, "application/json") 242 | } 243 | 244 | resSchema, err := op.ParseAPIObjectSchema("object", typeName(resErrer.Res()), fi.ASTFile) 245 | if err != nil { 246 | return err 247 | } 248 | 249 | errSchema, err := op.ParseAPIObjectSchema("object", typeName(resErrer.Err()), fi.ASTFile) 250 | if err != nil { 251 | return err 252 | } 253 | 254 | responses := op.Responses 255 | if responses == nil { 256 | responses = &spec.Responses{} 257 | op.Responses = responses 258 | } 259 | noErrors := true 260 | noResponses := true 261 | for code := range op.Responses.StatusCodeResponses { 262 | if code < http.StatusBadRequest { 263 | noResponses = false 264 | updateResponseSchema(&op.Operation, responses, code, resSchema) 265 | } 266 | 267 | if code >= http.StatusBadRequest { 268 | noErrors = false 269 | updateResponseSchema(&op.Operation, responses, code, errSchema) 270 | } 271 | } 272 | if noResponses { 273 | op.RespondsWith(http.StatusOK, spec.NewResponse().WithSchema(resSchema)) 274 | } 275 | if noErrors { 276 | op.RespondsWith(0, spec.NewResponse().WithSchema(errSchema)) 277 | } 278 | 279 | return nil 280 | } 281 | 282 | func typeName(i any) string { 283 | t := reflect.TypeOf(i) 284 | 285 | for t.Kind() == reflect.Pointer { 286 | t = t.Elem() 287 | } 288 | 289 | s := strings.ReplaceAll(t.String(), " ", "") 290 | s = strings.ReplaceAll(s, "*", "") 291 | 292 | if s == "error" { 293 | return "string" 294 | } 295 | 296 | return s 297 | } 298 | 299 | func updateResponseSchema(op *spec.Operation, responses *spec.Responses, code int, schema *spec.Schema) { 300 | s := op.Responses.StatusCodeResponses[code] 301 | 302 | if s.Schema != nil { 303 | return 304 | } 305 | 306 | s.Schema = schema 307 | 308 | op.Responses.StatusCodeResponses[code] = s 309 | } 310 | -------------------------------------------------------------------------------- /openapi2/openapi2_test.go: -------------------------------------------------------------------------------- 1 | package openapi2 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/go-chai/chai/chai" 11 | "github.com/go-chai/chai/internal/tests" 12 | "github.com/go-chai/swag" 13 | "github.com/go-openapi/spec" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestParseAPIObjectSchema(t *testing.T) { 19 | type args struct { 20 | val any 21 | } 22 | type want struct { 23 | typeName string 24 | ref string 25 | definitionsJSON string 26 | } 27 | tests := []struct { 28 | name string 29 | args args 30 | want want 31 | }{ 32 | { 33 | name: "string", 34 | args: args{ 35 | val: "12345asdf", 36 | }, 37 | want: want{typeName: "string"}, 38 | }, 39 | { 40 | name: "int", 41 | args: args{ 42 | val: 123, 43 | }, 44 | want: want{typeName: "integer"}, 45 | }, 46 | { 47 | name: "obj", 48 | args: args{ 49 | val: &tests.TestStruct{}, 50 | }, 51 | want: want{ref: "#/definitions/tests.TestStruct", definitionsJSON: `{"tests.TestStruct": {"type": "object","properties": {"bar": {"type": "integer"},"foo": {"type": "string"}}}}`}, 52 | }, 53 | } 54 | for _, tt := range tests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | parser := swag.New(swag.SetDebugger(log.Default()), func(p *swag.Parser) { 57 | p.ParseDependency = true 58 | }) 59 | fi := getFuncInfo(RegisterRoute) 60 | op := swag.NewOperation(parser) 61 | 62 | err := parser.GetAllGoFileInfoAndParseTypes("../internal") 63 | require.NoError(t, err) 64 | 65 | schema, err := op.ParseAPIObjectSchema("object", typeName(tt.args.val), fi.ASTFile) 66 | require.NoError(t, err) 67 | 68 | LogYAML(parser.GetSwagger().Definitions) 69 | 70 | if tt.want.typeName != "" { 71 | require.Equal(t, tt.want.typeName, schema.Type[0]) 72 | } 73 | 74 | if tt.want.ref != "" { 75 | require.Equal(t, tt.want.ref, schema.Ref.String()) 76 | } 77 | 78 | if tt.want.definitionsJSON != "" { 79 | require.JSONEq(t, tt.want.definitionsJSON, js(parser.GetSwagger().Definitions)) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestMergeParameters(t *testing.T) { 86 | type args struct { 87 | params [][]spec.Parameter 88 | } 89 | tests := []struct { 90 | name string 91 | args args 92 | want []spec.Parameter 93 | }{ 94 | { 95 | name: "test 1", 96 | args: args{ 97 | params: [][]spec.Parameter{ 98 | { 99 | {ParamProps: spec.ParamProps{Name: "p1", In: "path", Description: "d1", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t1", Format: "f1"}}, 100 | {ParamProps: spec.ParamProps{Name: "p3", In: "path", Description: "d3", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t3", Format: "f3"}}, 101 | }, 102 | { 103 | {ParamProps: spec.ParamProps{Name: "p1", In: "path", Description: "d11", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t11", Format: "f11"}}, 104 | }, 105 | }, 106 | }, 107 | want: []spec.Parameter{ 108 | {ParamProps: spec.ParamProps{Name: "p1", In: "path", Description: "d11", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t11", Format: "f11"}}, 109 | {ParamProps: spec.ParamProps{Name: "p3", In: "path", Description: "d3", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t3", Format: "f3"}}, 110 | }, 111 | }, 112 | { 113 | name: "t2", 114 | args: args{ 115 | params: [][]spec.Parameter{ 116 | { 117 | {ParamProps: spec.ParamProps{Name: "p1", In: "path", Description: "d1", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t1", Format: "f1"}}, 118 | {ParamProps: spec.ParamProps{Name: "p3", In: "path", Description: "d3", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t3", Format: "f3"}}, 119 | }, 120 | { 121 | {ParamProps: spec.ParamProps{Name: "p1", In: "body", Description: "d11", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t11", Format: "f11"}}, 122 | }, 123 | }, 124 | }, 125 | want: []spec.Parameter{ 126 | {ParamProps: spec.ParamProps{Name: "p1", In: "body", Description: "d11", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t11", Format: "f11"}}, 127 | {ParamProps: spec.ParamProps{Name: "p1", In: "path", Description: "d1", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t1", Format: "f1"}}, 128 | {ParamProps: spec.ParamProps{Name: "p3", In: "path", Description: "d3", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t3", Format: "f3"}}, 129 | }, 130 | }, 131 | { 132 | name: "t3", 133 | args: args{ 134 | params: [][]spec.Parameter{ 135 | { 136 | {ParamProps: spec.ParamProps{Name: "p1", In: "path", Description: "d1", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t1", Format: "f1"}}, 137 | {ParamProps: spec.ParamProps{Name: "p3", In: "path", Description: "d3", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t3", Format: "f3"}}, 138 | }, 139 | {{ParamProps: spec.ParamProps{Name: "p1", In: "body", Description: "d11", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t11", Format: "f11"}}}, 140 | }, 141 | }, 142 | want: []spec.Parameter{ 143 | {ParamProps: spec.ParamProps{Name: "p1", In: "body", Description: "d11", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t11", Format: "f11"}}, 144 | {ParamProps: spec.ParamProps{Name: "p1", In: "path", Description: "d1", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t1", Format: "f1"}}, 145 | {ParamProps: spec.ParamProps{Name: "p3", In: "path", Description: "d3", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t3", Format: "f3"}}, 146 | }, 147 | }, 148 | { 149 | name: "t4", 150 | args: args{ 151 | params: [][]spec.Parameter{ 152 | { 153 | {ParamProps: spec.ParamProps{Name: "p1", In: "path", Description: "d1", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t1", Format: "f1"}}, 154 | {ParamProps: spec.ParamProps{Name: "p3", In: "path", Description: "d3", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t3", Format: "f3"}}, 155 | }, 156 | {{ParamProps: spec.ParamProps{Name: "p1", In: "body", Description: "d11", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t11", Format: "f11"}}}, 157 | }, 158 | }, 159 | want: []spec.Parameter{ 160 | {ParamProps: spec.ParamProps{Name: "p1", In: "body", Description: "d11", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t11", Format: "f11"}}, 161 | {ParamProps: spec.ParamProps{Name: "p1", In: "path", Description: "d1", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t1", Format: "f1"}}, 162 | {ParamProps: spec.ParamProps{Name: "p3", In: "path", Description: "d3", Required: true}, SimpleSchema: spec.SimpleSchema{Type: "t3", Format: "f3"}}, 163 | }, 164 | }, 165 | } 166 | for _, tt := range tests { 167 | t.Run(tt.name, func(t *testing.T) { 168 | wantJSON := js(tt.want) 169 | got := mergeParameters(tt.args.params...) 170 | gotJSON := js(got) 171 | 172 | assert.JSONEq(t, string(wantJSON), string(gotJSON)) 173 | }) 174 | } 175 | } 176 | 177 | func js(v any) string { 178 | b, _ := json.MarshalIndent(v, "", " ") 179 | return string(b) 180 | } 181 | 182 | func TestAssociateBy(t *testing.T) { 183 | type args struct { 184 | ts []spec.Parameter 185 | fn func(spec.Parameter) pk 186 | } 187 | tests := []struct { 188 | name string 189 | args args 190 | want map[pk]spec.Parameter 191 | }{ 192 | { 193 | name: "t1", 194 | args: args{ 195 | ts: []spec.Parameter{ 196 | {ParamProps: spec.ParamProps{Name: "p1", In: "path", Description: "d1", Required: true}}, 197 | {ParamProps: spec.ParamProps{Name: "p2", In: "path", Description: "d2", Required: true}}, 198 | {ParamProps: spec.ParamProps{Name: "p1", In: "body", Description: "d11", Required: true}}, 199 | }, 200 | fn: func(p spec.Parameter) pk { 201 | return pk{p.In, p.Name} 202 | }, 203 | }, 204 | want: map[pk]spec.Parameter{ 205 | {In: "path", Name: "p1"}: {ParamProps: spec.ParamProps{Name: "p1", In: "path", Description: "d1", Required: true}}, 206 | {In: "path", Name: "p2"}: {ParamProps: spec.ParamProps{Name: "p2", In: "path", Description: "d2", Required: true}}, 207 | {In: "body", Name: "p1"}: {ParamProps: spec.ParamProps{Name: "p1", In: "body", Description: "d11", Required: true}}, 208 | }, 209 | }, 210 | } 211 | for _, tt := range tests { 212 | t.Run(tt.name, func(t *testing.T) { 213 | got := associateBy(tt.args.ts, tt.args.fn) 214 | 215 | assert.Equal(t, tt.want, got) 216 | }) 217 | } 218 | } 219 | 220 | func TestSortedKeys(t *testing.T) { 221 | type args struct { 222 | m map[pk]string 223 | less func(pk, pk) bool 224 | } 225 | tests := []struct { 226 | name string 227 | args args 228 | want []pk 229 | }{ 230 | { 231 | name: "t1", 232 | args: args{ 233 | m: map[pk]string{ 234 | {"path", "p1"}: "1", 235 | {"path", "p2"}: "2", 236 | {"body", "p3"}: "3", 237 | {"body", "p2"}: "4", 238 | }, 239 | less: less, 240 | }, 241 | want: []pk{ 242 | {"body", "p2"}, 243 | {"body", "p3"}, 244 | {"path", "p1"}, 245 | {"path", "p2"}, 246 | }, 247 | }, 248 | } 249 | for _, tt := range tests { 250 | t.Run(tt.name, func(t *testing.T) { 251 | got := sortedKeys(tt.args.m, tt.args.less) 252 | 253 | assert.Equal(t, tt.want, got) 254 | }) 255 | } 256 | } 257 | 258 | func TestDocs(t *testing.T) { 259 | type args struct { 260 | routes []*Route 261 | } 262 | tests := []struct { 263 | name string 264 | args args 265 | filePath string 266 | wantErr bool 267 | }{ 268 | { 269 | name: "t1", 270 | args: args{ 271 | routes: []*Route{ 272 | { 273 | Method: "GET", 274 | Path: "/test1/{p1}/{p2}", 275 | Params: []spec.Parameter{ 276 | {ParamProps: spec.ParamProps{Name: "p1", In: "path", Description: "d1", Required: true}}, 277 | {ParamProps: spec.ParamProps{Name: "p2", In: "path", Description: "d2", Required: true}}, 278 | }, 279 | 280 | // ShowBottle godoc 281 | // @Summary Test Handler 282 | // @Description get string by ID 283 | // @ID get-string-by-int 284 | // @Tags bottles 285 | // @Accept json 286 | // @Produce json 287 | // @Success 200 288 | // @Failure 400,404,500 289 | Handler: chai.NewReqResHandler(func(req *tests.TestRequest, w http.ResponseWriter, r *http.Request) (*tests.TestResponse, int, error) { 290 | return nil, 0, nil 291 | }), 292 | }, 293 | }, 294 | }, 295 | filePath: "testdata/t1.json", 296 | wantErr: false, 297 | }, 298 | { 299 | name: "t2", 300 | args: args{ 301 | routes: []*Route{ 302 | { 303 | Method: "GET", 304 | Path: "/test1/{p1}/{p2}", 305 | Params: []spec.Parameter{ 306 | {ParamProps: spec.ParamProps{Name: "p1", In: "path", Description: "d1", Required: true}}, 307 | {ParamProps: spec.ParamProps{Name: "p2", In: "path", Description: "d2", Required: true}}, 308 | }, 309 | 310 | // ShowBottles godoc 311 | // @Summary Test Handler 312 | // @Description get string by ID 313 | // @ID get-string-by-int 314 | // @Tags bottles 315 | // @Success 200 316 | // @Failure 400,404,500 317 | Handler: chai.NewReqResHandler(func(req []*tests.TestRequest, w http.ResponseWriter, r *http.Request) ([]*tests.TestResponse, int, error) { 318 | return nil, 0, nil 319 | }), 320 | }, 321 | }, 322 | }, 323 | filePath: "testdata/t2.json", 324 | wantErr: false, 325 | }, 326 | } 327 | for _, tt := range tests { 328 | t.Run(tt.name, func(t *testing.T) { 329 | got, err := Docs(tt.args.routes) 330 | 331 | LogJSON(got) 332 | 333 | if (err != nil) != tt.wantErr { 334 | t.Errorf("Docs() error = %v, wantErr %v", err, tt.wantErr) 335 | return 336 | } 337 | require.JSONEq(t, load(t, tt.filePath), js(got)) 338 | }) 339 | } 340 | } 341 | 342 | func load(t *testing.T, path string) string { 343 | b, err := ioutil.ReadFile(path) 344 | require.NoError(t, err) 345 | return string(b) 346 | } 347 | -------------------------------------------------------------------------------- /openapi2/params.go: -------------------------------------------------------------------------------- 1 | package openapi2 2 | 3 | -------------------------------------------------------------------------------- /openapi2/spec.go: -------------------------------------------------------------------------------- 1 | package openapi2 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-openapi/spec" 8 | "github.com/go-chai/swag" 9 | ) 10 | 11 | func newSpec() *spec.Swagger { 12 | return &spec.Swagger{ 13 | SwaggerProps: spec.SwaggerProps{ 14 | Info: &spec.Info{ 15 | InfoProps: spec.InfoProps{ 16 | Contact: &spec.ContactInfo{}, 17 | License: nil, 18 | }, 19 | VendorExtensible: spec.VendorExtensible{ 20 | Extensions: spec.Extensions{}, 21 | }, 22 | }, 23 | Paths: &spec.Paths{ 24 | Paths: make(map[string]spec.PathItem), 25 | }, 26 | Definitions: make(map[string]spec.Schema), 27 | SecurityDefinitions: make(map[string]*spec.SecurityScheme), 28 | }, 29 | } 30 | } 31 | 32 | func addOperation(swagger *spec.Swagger, path string, method string, operation *swag.Operation) { 33 | paths := swagger.Paths 34 | if paths == nil { 35 | paths = &spec.Paths{} 36 | swagger.Paths = paths 37 | } 38 | 39 | if paths.Paths == nil { 40 | paths.Paths = make(map[string]spec.PathItem) 41 | } 42 | 43 | // doc.Paths.Paths 44 | 45 | pathItem := paths.Paths[path] 46 | SetOperation(&pathItem, method, &operation.Operation) 47 | paths.Paths[path] = pathItem 48 | } 49 | 50 | func SetOperation(pathItem *spec.PathItem, method string, operation *spec.Operation) { 51 | switch method { 52 | case http.MethodDelete: 53 | pathItem.Delete = operation 54 | case http.MethodGet: 55 | pathItem.Get = operation 56 | case http.MethodHead: 57 | pathItem.Head = operation 58 | case http.MethodOptions: 59 | pathItem.Options = operation 60 | case http.MethodPatch: 61 | pathItem.Patch = operation 62 | case http.MethodPost: 63 | pathItem.Post = operation 64 | case http.MethodPut: 65 | pathItem.Put = operation 66 | default: 67 | panic(fmt.Errorf("unsupported HTTP method %q", method)) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /openapi2/testdata/t1.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "contact": {} 4 | }, 5 | "paths": { 6 | "/test1/{p1}/{p2}": { 7 | "get": { 8 | "description": "get string by ID", 9 | "consumes": [ 10 | "application/json" 11 | ], 12 | "produces": [ 13 | "application/json" 14 | ], 15 | "tags": [ 16 | "bottles" 17 | ], 18 | "summary": "Test Handler", 19 | "operationId": "get-string-by-int", 20 | "parameters": [ 21 | { 22 | "name": "body", 23 | "in": "body", 24 | "schema": { 25 | "$ref": "#/definitions/tests.TestRequest" 26 | } 27 | }, 28 | { 29 | "description": "d1", 30 | "name": "p1", 31 | "in": "path", 32 | "required": true 33 | }, 34 | { 35 | "description": "d2", 36 | "name": "p2", 37 | "in": "path", 38 | "required": true 39 | } 40 | ], 41 | "responses": { 42 | "200": { 43 | "description": "", 44 | "schema": { 45 | "$ref": "#/definitions/tests.TestResponse" 46 | } 47 | }, 48 | "400": { 49 | "description": "", 50 | "schema": { 51 | "type": "string" 52 | } 53 | }, 54 | "404": { 55 | "description": "", 56 | "schema": { 57 | "type": "string" 58 | } 59 | }, 60 | "500": { 61 | "description": "", 62 | "schema": { 63 | "type": "string" 64 | } 65 | } 66 | } 67 | } 68 | } 69 | }, 70 | "definitions": { 71 | "tests.TestInnerResponse": { 72 | "type": "object", 73 | "properties": { 74 | "bar_bar": { 75 | "type": "integer" 76 | }, 77 | "foo_foo": { 78 | "type": "integer" 79 | } 80 | } 81 | }, 82 | "tests.TestRequest": { 83 | "type": "object", 84 | "properties": { 85 | "barb": { 86 | "type": "string" 87 | }, 88 | "foob": { 89 | "type": "string" 90 | }, 91 | "test_inner_responseb": { 92 | "$ref": "#/definitions/tests.TestInnerResponse" 93 | } 94 | } 95 | }, 96 | "tests.TestResponse": { 97 | "type": "object", 98 | "properties": { 99 | "bar": { 100 | "type": "string" 101 | }, 102 | "foo": { 103 | "type": "string" 104 | }, 105 | "test_inner_response": { 106 | "$ref": "#/definitions/tests.TestInnerResponse" 107 | } 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /openapi2/testdata/t2.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "contact": {} 4 | }, 5 | "paths": { 6 | "/test1/{p1}/{p2}": { 7 | "get": { 8 | "description": "get string by ID", 9 | "consumes": [ 10 | "application/json" 11 | ], 12 | "produces": [ 13 | "application/json" 14 | ], 15 | "tags": [ 16 | "bottles" 17 | ], 18 | "summary": "Test Handler", 19 | "operationId": "get-string-by-int", 20 | "parameters": [ 21 | { 22 | "name": "body", 23 | "in": "body", 24 | "schema": { 25 | "type": "array", 26 | "items": { 27 | "$ref": "#/definitions/tests.TestRequest" 28 | } 29 | } 30 | }, 31 | { 32 | "description": "d1", 33 | "name": "p1", 34 | "in": "path", 35 | "required": true 36 | }, 37 | { 38 | "description": "d2", 39 | "name": "p2", 40 | "in": "path", 41 | "required": true 42 | } 43 | ], 44 | "responses": { 45 | "200": { 46 | "description": "", 47 | "schema": { 48 | "type": "array", 49 | "items": { 50 | "$ref": "#/definitions/tests.TestResponse" 51 | } 52 | } 53 | }, 54 | "400": { 55 | "description": "", 56 | "schema": { 57 | "type": "string" 58 | } 59 | }, 60 | "404": { 61 | "description": "", 62 | "schema": { 63 | "type": "string" 64 | } 65 | }, 66 | "500": { 67 | "description": "", 68 | "schema": { 69 | "type": "string" 70 | } 71 | } 72 | } 73 | } 74 | } 75 | }, 76 | "definitions": { 77 | "tests.TestInnerResponse": { 78 | "type": "object", 79 | "properties": { 80 | "bar_bar": { 81 | "type": "integer" 82 | }, 83 | "foo_foo": { 84 | "type": "integer" 85 | } 86 | } 87 | }, 88 | "tests.TestRequest": { 89 | "type": "object", 90 | "properties": { 91 | "barb": { 92 | "type": "string" 93 | }, 94 | "foob": { 95 | "type": "string" 96 | }, 97 | "test_inner_responseb": { 98 | "$ref": "#/definitions/tests.TestInnerResponse" 99 | } 100 | } 101 | }, 102 | "tests.TestResponse": { 103 | "type": "object", 104 | "properties": { 105 | "bar": { 106 | "type": "string" 107 | }, 108 | "foo": { 109 | "type": "string" 110 | }, 111 | "test_inner_response": { 112 | "$ref": "#/definitions/tests.TestInnerResponse" 113 | } 114 | } 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /openapi3/gen.go: -------------------------------------------------------------------------------- 1 | package openapi3 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "go/format" 9 | "io" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "text/template" 15 | "time" 16 | 17 | "github.com/getkin/kin-openapi/openapi3" 18 | "github.com/ghodss/yaml" 19 | "github.com/swaggo/swag" 20 | ) 21 | 22 | var open = os.Open 23 | 24 | // DefaultOverridesFile is the location swaggo will look for type overrides. 25 | const DefaultOverridesFile = ".swaggo" 26 | 27 | // Gen presents a generate tool for swag. 28 | type Gen struct { 29 | jsonIndent func(data interface{}) ([]byte, error) 30 | jsonToYAML func(data []byte) ([]byte, error) 31 | } 32 | 33 | // New creates a new Gen. 34 | func NewGen() *Gen { 35 | return &Gen{ 36 | jsonIndent: func(data interface{}) ([]byte, error) { 37 | return json.MarshalIndent(data, "", " ") 38 | }, 39 | jsonToYAML: yaml.JSONToYAML, 40 | } 41 | } 42 | 43 | // Config presents Gen configurations. 44 | type Config struct { 45 | // SearchDir the swag would be parse,comma separated if multiple 46 | SearchDir string 47 | 48 | // excludes dirs and files in SearchDir,comma separated 49 | Excludes string 50 | 51 | // OutputDir represents the output directory for all the generated files 52 | OutputDir string 53 | 54 | // MainAPIFile the Go file path in which 'swagger general API Info' is written 55 | MainAPIFile string 56 | 57 | // PropNamingStrategy represents property naming strategy like snake case,camel case,pascal case 58 | PropNamingStrategy string 59 | 60 | // MarkdownFilesDir used to find markdown files, which can be used for tag descriptions 61 | MarkdownFilesDir string 62 | 63 | // CodeExampleFilesDir used to find code example files, which can be used for x-codeSamples 64 | CodeExampleFilesDir string 65 | 66 | // InstanceName is used to get distinct names for different swagger documents in the 67 | // same project. The default value is "swagger". 68 | InstanceName string 69 | 70 | // ParseDepth dependency parse depth 71 | ParseDepth int 72 | 73 | // ParseVendor whether swag should be parse vendor folder 74 | ParseVendor bool 75 | 76 | // ParseDependencies whether swag should be parse outside dependency folder 77 | ParseDependency bool 78 | 79 | // ParseInternal whether swag should parse internal packages 80 | ParseInternal bool 81 | 82 | // Strict whether swag should error or warn when it detects cases which are most likely user errors 83 | Strict bool 84 | 85 | // GeneratedTime whether swag should generate the timestamp at the top of docs.go 86 | GeneratedTime bool 87 | 88 | // OverridesFile defines global type overrides. 89 | OverridesFile string 90 | } 91 | 92 | // Generate outputs a swagger spec 93 | func (g *Gen) Generate(swagger *openapi3.T, config *GenConfig) error { 94 | if config.InstanceName == "" { 95 | config.InstanceName = swag.Name 96 | } 97 | 98 | if config.OutputDir == "" { 99 | config.OutputDir = "docs/" 100 | } 101 | 102 | b, err := g.jsonIndent(swagger) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | if err := os.MkdirAll(config.OutputDir, os.ModePerm); err != nil { 108 | return err 109 | } 110 | 111 | absOutputDir, err := filepath.Abs(config.OutputDir) 112 | if err != nil { 113 | return err 114 | } 115 | packageName := filepath.Base(absOutputDir) 116 | docFileName := filepath.Join(config.OutputDir, "docs.go") 117 | jsonFileName := filepath.Join(config.OutputDir, "swagger.json") 118 | yamlFileName := filepath.Join(config.OutputDir, "swagger.yaml") 119 | 120 | docs, err := os.Create(docFileName) 121 | if err != nil { 122 | return err 123 | } 124 | defer docs.Close() 125 | 126 | err = g.writeFile(b, jsonFileName) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | y, err := g.jsonToYAML(b) 132 | if err != nil { 133 | return fmt.Errorf("cannot convert json to yaml error: %s", err) 134 | } 135 | 136 | err = g.writeFile(y, yamlFileName) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | // Write doc 142 | err = g.writeGoDoc(packageName, docs, swagger, config) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | log.Printf("create docs.go at %+v", docFileName) 148 | log.Printf("create swagger.json at %+v", jsonFileName) 149 | log.Printf("create swagger.yaml at %+v", yamlFileName) 150 | 151 | return nil 152 | } 153 | 154 | func (g *Gen) writeFile(b []byte, file string) error { 155 | f, err := os.Create(file) 156 | if err != nil { 157 | return err 158 | } 159 | defer f.Close() 160 | 161 | _, err = f.Write(b) 162 | return err 163 | } 164 | 165 | func (g *Gen) formatSource(src []byte) []byte { 166 | code, err := format.Source(src) 167 | if err != nil { 168 | code = src // Output the unformatted code anyway 169 | } 170 | return code 171 | } 172 | 173 | // Read the swaggo overrides 174 | func parseOverrides(r io.Reader) (map[string]string, error) { 175 | overrides := make(map[string]string) 176 | scanner := bufio.NewScanner(r) 177 | 178 | for scanner.Scan() { 179 | line := scanner.Text() 180 | 181 | // Skip comments 182 | if len(line) > 1 && line[0:2] == "//" { 183 | continue 184 | } 185 | 186 | parts := strings.Fields(line) 187 | 188 | switch len(parts) { 189 | case 0: 190 | // only whitespace 191 | continue 192 | case 2: 193 | // either a skip or malformed 194 | if parts[0] != "skip" { 195 | return nil, fmt.Errorf("could not parse override: '%s'", line) 196 | } 197 | overrides[parts[1]] = "" 198 | case 3: 199 | // either a replace or malformed 200 | if parts[0] != "replace" { 201 | return nil, fmt.Errorf("could not parse override: '%s'", line) 202 | } 203 | overrides[parts[1]] = parts[2] 204 | default: 205 | return nil, fmt.Errorf("could not parse override: '%s'", line) 206 | } 207 | } 208 | 209 | if err := scanner.Err(); err != nil { 210 | return nil, fmt.Errorf("error reading overrides file: %w", err) 211 | } 212 | 213 | return overrides, nil 214 | } 215 | 216 | func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *openapi3.T, config *GenConfig) error { 217 | generator, err := template.New("swagger_info").Funcs(template.FuncMap{ 218 | "printDoc": func(v string) string { 219 | // Add servers 220 | v = "{\n \"servers\": {{ marshal .Servers }}," + v[1:] 221 | // Sanitize backticks 222 | return strings.Replace(v, "`", "`+\"`\"+`", -1) 223 | }, 224 | }).Parse(packageTemplate) 225 | if err != nil { 226 | return err 227 | } 228 | 229 | swaggerSpec := &openapi3.T{ 230 | ExtensionProps: swagger.ExtensionProps, 231 | OpenAPI: swagger.OpenAPI, 232 | Components: swagger.Components, 233 | Info: &openapi3.Info{ 234 | ExtensionProps: swagger.Info.ExtensionProps, 235 | Description: "{{escape .Description}}", 236 | Title: "{{.Title}}", 237 | TermsOfService: swagger.Info.TermsOfService, 238 | Contact: swagger.Info.Contact, 239 | License: swagger.Info.License, 240 | Version: "{{.Version}}", 241 | }, 242 | Paths: swagger.Paths, 243 | Security: swagger.Security, 244 | Tags: swagger.Tags, 245 | ExternalDocs: swagger.ExternalDocs, 246 | } 247 | 248 | // crafted docs.json 249 | buf, err := g.jsonIndent(swaggerSpec) 250 | if err != nil { 251 | return err 252 | } 253 | 254 | buffer := &bytes.Buffer{} 255 | err = generator.Execute(buffer, struct { 256 | Timestamp time.Time 257 | Doc string 258 | Servers openapi3.Servers 259 | PackageName string 260 | Title string 261 | Description string 262 | Version string 263 | InstanceName string 264 | GeneratedTime bool 265 | }{ 266 | Timestamp: time.Now(), 267 | GeneratedTime: config.GeneratedTime, 268 | Doc: string(buf), 269 | Servers: swagger.Servers, 270 | PackageName: packageName, 271 | Title: swagger.Info.Title, 272 | Description: swagger.Info.Description, 273 | Version: swagger.Info.Version, 274 | InstanceName: config.InstanceName, 275 | }) 276 | if err != nil { 277 | return err 278 | } 279 | 280 | code := g.formatSource(buffer.Bytes()) 281 | 282 | // write 283 | _, err = output.Write(code) 284 | return err 285 | } 286 | 287 | var packageTemplate = `// Package {{.PackageName}} GENERATED BY THE COMMAND ABOVE; DO NOT EDIT 288 | // This file was generated by swaggo/swag{{ if .GeneratedTime }} at 289 | // {{ .Timestamp }}{{ end }} 290 | package {{.PackageName}} 291 | 292 | import ( 293 | "bytes" 294 | "encoding/json" 295 | "strings" 296 | "text/template" 297 | 298 | "github.com/getkin/kin-openapi/openapi3" 299 | "github.com/swaggo/swag" 300 | ) 301 | 302 | var doc = ` + "`{{ printDoc .Doc}}`" + ` 303 | 304 | type swaggerInfo struct { 305 | Version string 306 | Servers openapi3.Servers 307 | Title string 308 | Description string 309 | } 310 | 311 | // SwaggerInfo holds exported Swagger Info so clients can modify it 312 | var SwaggerInfo = swaggerInfo{ 313 | Version: {{ printf "%q" .Version}}, 314 | Servers: openapi3.Servers{ 315 | {{ range $index, $server := .Servers}}{{if gt $index 0}},{{end}} &openapi3.Server{ 316 | URL: {{ printf "%q" $server.URL }}, 317 | Description: {{ printf "%q" $server.Description }}, 318 | }, 319 | {{end}} 320 | }, 321 | Title: {{ printf "%q" .Title}}, 322 | Description: {{ printf "%q" .Description}}, 323 | } 324 | 325 | type s struct{} 326 | 327 | func (s *s) ReadDoc() string { 328 | sInfo := SwaggerInfo 329 | sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) 330 | 331 | t, err := template.New("swagger_info").Funcs(template.FuncMap{ 332 | "marshal": func(v interface{}) string { 333 | a, _ := json.Marshal(v) 334 | return string(a) 335 | }, 336 | "escape": func(v interface{}) string { 337 | // escape tabs 338 | str := strings.Replace(v.(string), "\t", "\\t", -1) 339 | // replace " with \", and if that results in \\", replace that with \\\" 340 | str = strings.Replace(str, "\"", "\\\"", -1) 341 | return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1) 342 | }, 343 | }).Parse(doc) 344 | if err != nil { 345 | return doc 346 | } 347 | 348 | var tpl bytes.Buffer 349 | if err := t.Execute(&tpl, sInfo); err != nil { 350 | return doc 351 | } 352 | 353 | return tpl.String() 354 | } 355 | 356 | func init() { 357 | swag.Register({{ printf "%q" .InstanceName }}, &s{}) 358 | } 359 | ` 360 | -------------------------------------------------------------------------------- /openapi3/log.go: -------------------------------------------------------------------------------- 1 | package openapi3 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func LogYAML(v any) { 10 | bytes, err := MarshalYAML(v) 11 | if err != nil { 12 | panic(err) 13 | } 14 | 15 | fmt.Fprintln(os.Stdout, string(bytes)) 16 | 17 | return 18 | } 19 | 20 | func LogJSON(v any) { 21 | bytes, err := json.MarshalIndent(v, "", " ") 22 | 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | fmt.Println(string(bytes)) 28 | 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /openapi3/marshal.go: -------------------------------------------------------------------------------- 1 | package openapi3 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/ghodss/yaml" 7 | ) 8 | 9 | func MarshalYAML(v interface{}) ([]byte, error) { 10 | return yaml.Marshal(v) 11 | } 12 | 13 | func MarshalJSON(v interface{}) ([]byte, error) { 14 | return json.Marshal(v) 15 | } 16 | -------------------------------------------------------------------------------- /openapi3/openapi3.go: -------------------------------------------------------------------------------- 1 | package openapi3 2 | 3 | import ( 4 | kinopenapi2 "github.com/getkin/kin-openapi/openapi2" 5 | "github.com/getkin/kin-openapi/openapi2conv" 6 | "github.com/getkin/kin-openapi/openapi3" 7 | "github.com/ghodss/yaml" 8 | "github.com/go-chai/chai/openapi2" 9 | "github.com/go-chai/swag/gen" 10 | ) 11 | 12 | type GenConfig = gen.GenConfig 13 | 14 | func WriteDocs(docs *openapi3.T, cfg *GenConfig) error { 15 | return NewGen().Generate(docs, cfg) 16 | } 17 | 18 | type Route = openapi2.Route 19 | 20 | func Docs(routes []*Route) (*openapi3.T, error) { 21 | docs, err := openapi2.Docs(routes) 22 | if err != nil { 23 | return nil, err 24 | } 25 | docsJSON, err := openapi2.MarshalYAML(docs) 26 | if err != nil { 27 | return nil, err 28 | } 29 | kinOpenAPI2 := new(kinopenapi2.T) 30 | err = yaml.Unmarshal(docsJSON, kinOpenAPI2) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return openapi2conv.ToV3(kinOpenAPI2) 35 | } 36 | --------------------------------------------------------------------------------