├── .gitignore ├── LICENSE ├── README.md ├── api.go ├── chiadapter ├── route.go └── route_test.go ├── enums ├── enums.go └── enums_test.go ├── examples ├── chiexample │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── models │ │ └── models.go ├── offline │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── models │ │ └── models.go └── stdlib │ ├── go.mod │ ├── go.sum │ ├── handlers │ ├── topic │ │ └── post │ │ │ └── handler.go │ └── topics │ │ └── get │ │ └── handler.go │ ├── main.go │ └── models │ └── models.go ├── getcomments ├── main.go └── parser │ ├── README.md │ ├── parser.go │ ├── parser_test.go │ ├── snapshot │ └── main.go │ └── tests │ ├── anonymous │ ├── example.go │ └── snapshot.json │ ├── chans │ ├── example.go │ └── snapshot.json │ ├── docs │ ├── example.go │ └── snapshot.json │ ├── enum │ ├── example.go │ └── snapshot.json │ ├── functions │ ├── example.go │ └── snapshot.json │ ├── functiontypes │ ├── example.go │ └── snapshot.json │ ├── generics │ ├── example.go │ └── snapshot.json │ ├── maps │ ├── example.go │ └── snapshot.json │ ├── pointers │ ├── example.go │ └── snapshot.json │ ├── privatetypes │ ├── example.go │ └── snapshot.json │ └── publictypes │ ├── example.go │ └── snapshot.json ├── go.mod ├── go.sum ├── schema.go ├── schema_test.go ├── swaggerui ├── handler.go └── swagger-ui │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── index.css │ ├── index.html │ ├── oauth2-redirect.html │ ├── swagger-initializer.js │ ├── swagger-ui-bundle.js │ ├── swagger-ui-bundle.js.map │ ├── swagger-ui-es-bundle-core.js │ ├── swagger-ui-es-bundle-core.js.map │ ├── swagger-ui-es-bundle.js │ ├── swagger-ui-es-bundle.js.map │ ├── swagger-ui-standalone-preset.js │ ├── swagger-ui-standalone-preset.js.map │ ├── swagger-ui.css │ ├── swagger-ui.css.map │ ├── swagger-ui.js │ └── swagger-ui.js.map └── tests ├── all-methods.yaml ├── anonymous-type.yaml ├── basic-data-types-pointers.yaml ├── basic-data-types.yaml ├── custom-models.yaml ├── embedded-structs.yaml ├── enum-constants.yaml ├── enums.yaml ├── global-customisation.yaml ├── known-types.yaml ├── multiple-dates-with-comments.yaml ├── omit-empty-fields.yaml ├── query-params.yaml ├── route-params.yaml ├── test000.yaml ├── test001.yaml ├── with-maps.yaml └── with-name-struct-tags.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.out 2 | .idea 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Adrian Hesketh 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REST 2 | 3 | Document a REST API with an OpenAPI 3.0 specification. 4 | 5 | * Code, not configuration. 6 | * No magic comments, tags, or decorators. 7 | * Use with or without a Go web framework. 8 | * Populates schema automatically using reflection. 9 | 10 | ## Why would I want to use this? 11 | 12 | * Add OpenAPI documentation to an API. 13 | * Create a `swagger.json` or `swagger.yaml` file. 14 | * Serve the Swagger UI to customers. 15 | 16 | ## Examples 17 | 18 | See the [./examples](./examples) directory for complete examples. 19 | 20 | ### Create an OpenAPI 3.0 (swagger) file 21 | 22 | ```go 23 | // Configure the models. 24 | api := rest.NewAPI("messages") 25 | api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} 26 | 27 | api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { 28 | status := s.Properties["statusCode"] 29 | status.Value.WithMin(100).WithMax(600) 30 | }) 31 | 32 | api.Get("/topic/{id}"). 33 | HasPathParameter("id", rest.PathParam{ 34 | Description: "id of the topic", 35 | Regexp: `\d+`, 36 | }). 37 | HasResponseModel(http.StatusOK, rest.ModelOf[models.Topic]()). 38 | HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) 39 | 40 | // Create the specification. 41 | spec, err := api.Spec() 42 | if err != nil { 43 | log.Fatalf("failed to create spec: %v", err) 44 | } 45 | 46 | // Write to stdout. 47 | enc := json.NewEncoder(os.Stdout) 48 | enc.SetIndent("", " ") 49 | enc.Encode(spec) 50 | ``` 51 | 52 | ### Serve API documentation alongside your API 53 | 54 | ```go 55 | // Create routes. 56 | router := http.NewServeMux() 57 | router.Handle("/topics", &get.Handler{}) 58 | router.Handle("/topic", &post.Handler{}) 59 | 60 | api := rest.NewAPI("messages") 61 | api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} 62 | 63 | // Register the error type with customisations. 64 | api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { 65 | status := s.Properties["statusCode"] 66 | status.Value.WithMin(100).WithMax(600) 67 | }) 68 | 69 | api.Get("/topics"). 70 | HasResponseModel(http.StatusOK, rest.ModelOf[get.TopicsGetResponse]()). 71 | HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) 72 | 73 | api.Post("/topic"). 74 | HasRequestModel(rest.ModelOf[post.TopicPostRequest]()). 75 | HasResponseModel(http.StatusOK, rest.ModelOf[post.TopicPostResponse]()). 76 | HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) 77 | 78 | // Create the spec. 79 | spec, err := api.Spec() 80 | if err != nil { 81 | log.Fatalf("failed to create spec: %v", err) 82 | } 83 | 84 | // Apply any global customisation. 85 | spec.Info.Version = "v1.0.0." 86 | spec.Info.Description = "Messages API" 87 | 88 | // Attach the Swagger UI handler to your router. 89 | ui, err := swaggerui.New(spec) 90 | if err != nil { 91 | log.Fatalf("failed to create swagger UI handler: %v", err) 92 | } 93 | router.Handle("/swagger-ui", ui) 94 | router.Handle("/swagger-ui/", ui) 95 | 96 | // And start listening. 97 | fmt.Println("Listening on :8080...") 98 | fmt.Println("Visit http://localhost:8080/swagger-ui to see API definitions") 99 | fmt.Println("Listening on :8080...") 100 | http.ListenAndServe(":8080", router) 101 | ``` 102 | 103 | ## Tasks 104 | 105 | ### test 106 | 107 | ``` 108 | go test ./... 109 | ``` 110 | 111 | ### run-example 112 | 113 | Dir: ./examples/stdlib 114 | 115 | ``` 116 | go run main.go 117 | ``` 118 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "time" 7 | 8 | "github.com/getkin/kin-openapi/openapi3" 9 | ) 10 | 11 | type APIOpts func(*API) 12 | 13 | // WithApplyCustomSchemaToType enables customisation of types in the OpenAPI specification. 14 | // Apply customisation to a specific type by checking the t parameter. 15 | // Apply customisations to all types by ignoring the t parameter. 16 | func WithApplyCustomSchemaToType(f func(t reflect.Type, s *openapi3.Schema)) APIOpts { 17 | return func(api *API) { 18 | api.ApplyCustomSchemaToType = f 19 | } 20 | } 21 | 22 | // NewAPI creates a new API from the router. 23 | func NewAPI(name string, opts ...APIOpts) *API { 24 | api := &API{ 25 | Name: name, 26 | KnownTypes: defaultKnownTypes, 27 | Routes: make(map[Pattern]MethodToRoute), 28 | // map of model name to schema. 29 | models: make(map[string]*openapi3.Schema), 30 | comments: make(map[string]map[string]string), 31 | } 32 | for _, o := range opts { 33 | o(api) 34 | } 35 | return api 36 | } 37 | 38 | var defaultKnownTypes = map[reflect.Type]openapi3.Schema{ 39 | reflect.TypeOf(time.Time{}): *openapi3.NewDateTimeSchema(), 40 | reflect.TypeOf(&time.Time{}): *openapi3.NewDateTimeSchema().WithNullable(), 41 | } 42 | 43 | // Route models a single API route. 44 | type Route struct { 45 | // Method is the HTTP method of the route, e.g. http.MethodGet 46 | Method Method 47 | // Pattern of the route, e.g. /posts/list, or /users/{id} 48 | Pattern Pattern 49 | // Params of the route. 50 | Params Params 51 | // Models used in the route. 52 | Models Models 53 | // Tags used in the route. 54 | Tags []string 55 | // OperationID for the route. 56 | OperationID string 57 | // Description for the route. 58 | Description string 59 | } 60 | 61 | // Params is a route parameter. 62 | type Params struct { 63 | // Path parameters are used in the path of the URL, e.g. /users/{id} would 64 | // have a name of "id". 65 | Path map[string]PathParam 66 | // Query parameters are used in the querystring of the URL, e.g. /users/?sort={sortOrder} would 67 | // have a name of "sort". 68 | Query map[string]QueryParam 69 | } 70 | 71 | // PathParam is a paramater that's used in the path of a URL. 72 | type PathParam struct { 73 | // Description of the param. 74 | Description string 75 | // Regexp is a regular expression used to validate the param. 76 | // An empty string means that no validation is applied. 77 | Regexp string 78 | // Type of the param (string, number, integer, boolean). 79 | Type PrimitiveType 80 | // ApplyCustomSchema customises the OpenAPI schema for the path parameter. 81 | ApplyCustomSchema func(s *openapi3.Parameter) 82 | } 83 | 84 | // QueryParam is a paramater that's used in the querystring of a URL. 85 | type QueryParam struct { 86 | // Description of the param. 87 | Description string 88 | // Regexp is a regular expression used to validate the param. 89 | // An empty string means that no validation is applied. 90 | Regexp string 91 | // Required sets whether the querystring parameter must be present in the URL. 92 | Required bool 93 | // AllowEmpty sets whether the querystring parameter can be empty. 94 | AllowEmpty bool 95 | // Type of the param (string, number, integer, boolean). 96 | Type PrimitiveType 97 | // ApplyCustomSchema customises the OpenAPI schema for the query parameter. 98 | ApplyCustomSchema func(s *openapi3.Parameter) 99 | } 100 | 101 | type PrimitiveType string 102 | 103 | const ( 104 | PrimitiveTypeString PrimitiveType = "string" 105 | PrimitiveTypeBool PrimitiveType = "boolean" 106 | PrimitiveTypeInteger PrimitiveType = "integer" 107 | PrimitiveTypeFloat64 PrimitiveType = "number" 108 | ) 109 | 110 | // MethodToRoute maps from a HTTP method to a Route. 111 | type MethodToRoute map[Method]*Route 112 | 113 | // Method is the HTTP method of the route, e.g. http.MethodGet 114 | type Method string 115 | 116 | // Pattern of the route, e.g. /posts/list, or /users/{id} 117 | type Pattern string 118 | 119 | // API is a model of a REST API's routes, along with their 120 | // request and response types. 121 | type API struct { 122 | // Name of the API. 123 | Name string 124 | // Routes of the API. 125 | // From patterns, to methods, to route. 126 | Routes map[Pattern]MethodToRoute 127 | // StripPkgPaths to strip from the type names in the OpenAPI output to avoid 128 | // leaking internal implementation details such as internal repo names. 129 | // 130 | // This increases the risk of type clashes in the OpenAPI output, i.e. two types 131 | // in different namespaces that are set to be stripped, and have the same type Name 132 | // could clash. 133 | // 134 | // Example values could be "github.com/a-h/rest". 135 | StripPkgPaths []string 136 | 137 | // Models are the models that are in use in the API. 138 | // It's possible to customise the models prior to generation of the OpenAPI specification 139 | // by editing this value. 140 | models map[string]*openapi3.Schema 141 | 142 | // KnownTypes are added to the OpenAPI specification output. 143 | // The default implementation: 144 | // Maps time.Time to a string. 145 | KnownTypes map[reflect.Type]openapi3.Schema 146 | 147 | // comments from the package. This can be cleared once the spec has been created. 148 | comments map[string]map[string]string 149 | 150 | // ApplyCustomSchemaToType callback to customise the OpenAPI specification for a given type. 151 | // Apply customisation to a specific type by checking the t parameter. 152 | // Apply customisations to all types by ignoring the t parameter. 153 | ApplyCustomSchemaToType func(t reflect.Type, s *openapi3.Schema) 154 | } 155 | 156 | // Merge route data into the existing configuration. 157 | // This is typically used by adapters, such as the chiadapter 158 | // to take information that the router already knows and add it 159 | // to the specification. 160 | func (api *API) Merge(r Route) { 161 | toUpdate := api.Route(string(r.Method), string(r.Pattern)) 162 | mergeMap(toUpdate.Params.Path, r.Params.Path) 163 | mergeMap(toUpdate.Params.Query, r.Params.Query) 164 | if toUpdate.Models.Request.Type == nil { 165 | toUpdate.Models.Request = r.Models.Request 166 | } 167 | mergeMap(toUpdate.Models.Responses, r.Models.Responses) 168 | } 169 | 170 | func mergeMap[TKey comparable, TValue any](into, from map[TKey]TValue) { 171 | for kf, vf := range from { 172 | _, ok := into[kf] 173 | if !ok { 174 | into[kf] = vf 175 | } 176 | } 177 | } 178 | 179 | // Spec creates an OpenAPI 3.0 specification document for the API. 180 | func (api *API) Spec() (spec *openapi3.T, err error) { 181 | spec, err = api.createOpenAPI() 182 | if err != nil { 183 | return 184 | } 185 | return 186 | } 187 | 188 | // Route upserts a route to the API definition. 189 | func (api *API) Route(method, pattern string) (r *Route) { 190 | methodToRoute, ok := api.Routes[Pattern(pattern)] 191 | if !ok { 192 | methodToRoute = make(MethodToRoute) 193 | api.Routes[Pattern(pattern)] = methodToRoute 194 | } 195 | route, ok := methodToRoute[Method(method)] 196 | if !ok { 197 | route = &Route{ 198 | Method: Method(method), 199 | Pattern: Pattern(pattern), 200 | Models: Models{ 201 | Responses: make(map[int]Model), 202 | }, 203 | Params: Params{ 204 | Path: make(map[string]PathParam), 205 | Query: make(map[string]QueryParam), 206 | }, 207 | } 208 | methodToRoute[Method(method)] = route 209 | } 210 | return route 211 | } 212 | 213 | // Get defines a GET request route for the given pattern. 214 | func (api *API) Get(pattern string) (r *Route) { 215 | return api.Route(http.MethodGet, pattern) 216 | } 217 | 218 | // Head defines a HEAD request route for the given pattern. 219 | func (api *API) Head(pattern string) (r *Route) { 220 | return api.Route(http.MethodHead, pattern) 221 | } 222 | 223 | // Post defines a POST request route for the given pattern. 224 | func (api *API) Post(pattern string) (r *Route) { 225 | return api.Route(http.MethodPost, pattern) 226 | } 227 | 228 | // Put defines a PUT request route for the given pattern. 229 | func (api *API) Put(pattern string) (r *Route) { 230 | return api.Route(http.MethodPut, pattern) 231 | } 232 | 233 | // Patch defines a PATCH request route for the given pattern. 234 | func (api *API) Patch(pattern string) (r *Route) { 235 | return api.Route(http.MethodPatch, pattern) 236 | } 237 | 238 | // Delete defines a DELETE request route for the given pattern. 239 | func (api *API) Delete(pattern string) (r *Route) { 240 | return api.Route(http.MethodDelete, pattern) 241 | } 242 | 243 | // Connect defines a CONNECT request route for the given pattern. 244 | func (api *API) Connect(pattern string) (r *Route) { 245 | return api.Route(http.MethodConnect, pattern) 246 | } 247 | 248 | // Options defines an OPTIONS request route for the given pattern. 249 | func (api *API) Options(pattern string) (r *Route) { 250 | return api.Route(http.MethodOptions, pattern) 251 | } 252 | 253 | // Trace defines an TRACE request route for the given pattern. 254 | func (api *API) Trace(pattern string) (r *Route) { 255 | return api.Route(http.MethodTrace, pattern) 256 | } 257 | 258 | // HasResponseModel configures a response for the route. 259 | // Example: 260 | // 261 | // api.Get("/user").HasResponseModel(http.StatusOK, rest.ModelOf[User]()) 262 | func (rm *Route) HasResponseModel(status int, response Model) *Route { 263 | rm.Models.Responses[status] = response 264 | return rm 265 | } 266 | 267 | // HasResponseModel configures the request model of the route. 268 | // Example: 269 | // 270 | // api.Post("/user").HasRequestModel(http.StatusOK, rest.ModelOf[User]()) 271 | func (rm *Route) HasRequestModel(request Model) *Route { 272 | rm.Models.Request = request 273 | return rm 274 | } 275 | 276 | // HasPathParameter configures a path parameter for the route. 277 | func (rm *Route) HasPathParameter(name string, p PathParam) *Route { 278 | rm.Params.Path[name] = p 279 | return rm 280 | } 281 | 282 | // HasQueryParameter configures a query parameter for the route. 283 | func (rm *Route) HasQueryParameter(name string, q QueryParam) *Route { 284 | rm.Params.Query[name] = q 285 | return rm 286 | } 287 | 288 | // HasTags sets the tags for the route. 289 | func (rm *Route) HasTags(tags []string) *Route { 290 | rm.Tags = append(rm.Tags, tags...) 291 | return rm 292 | } 293 | 294 | // HasOperationID sets the OperationID for the route. 295 | func (rm *Route) HasOperationID(operationID string) *Route { 296 | rm.OperationID = operationID 297 | return rm 298 | } 299 | 300 | // HasDescription sets the description for the route. 301 | func (rm *Route) HasDescription(description string) *Route { 302 | rm.Description = description 303 | return rm 304 | } 305 | 306 | // Models defines the models used by a route. 307 | type Models struct { 308 | Request Model 309 | Responses map[int]Model 310 | } 311 | 312 | // ModelOf creates a model of type T. 313 | func ModelOf[T any]() Model { 314 | var t T 315 | m := Model{ 316 | Type: reflect.TypeOf(t), 317 | } 318 | if sm, ok := any(t).(CustomSchemaApplier); ok { 319 | m.s = sm.ApplyCustomSchema 320 | } 321 | return m 322 | } 323 | 324 | func modelFromType(t reflect.Type) Model { 325 | m := Model{ 326 | Type: t, 327 | } 328 | if sm, ok := reflect.New(t).Interface().(CustomSchemaApplier); ok { 329 | m.s = sm.ApplyCustomSchema 330 | } 331 | return m 332 | } 333 | 334 | // CustomSchemaApplier is a type that customises its OpenAPI schema. 335 | type CustomSchemaApplier interface { 336 | ApplyCustomSchema(s *openapi3.Schema) 337 | } 338 | 339 | var _ CustomSchemaApplier = Model{} 340 | 341 | // Model is a model used in one or more routes. 342 | type Model struct { 343 | Type reflect.Type 344 | s func(s *openapi3.Schema) 345 | } 346 | 347 | func (m Model) ApplyCustomSchema(s *openapi3.Schema) { 348 | if m.s == nil { 349 | return 350 | } 351 | m.s(s) 352 | } 353 | -------------------------------------------------------------------------------- /chiadapter/route.go: -------------------------------------------------------------------------------- 1 | package chiadapter 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/a-h/rest" 9 | "github.com/go-chi/chi/v5" 10 | ) 11 | 12 | func Merge(target *rest.API, src chi.Router) error { 13 | walker := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { 14 | params, err := getParams(route) 15 | if err != nil { 16 | return err 17 | } 18 | r := rest.Route{ 19 | Method: rest.Method(method), 20 | Pattern: rest.Pattern(route), 21 | Params: params, 22 | } 23 | target.Merge(r) 24 | return nil 25 | } 26 | 27 | return chi.Walk(src, walker) 28 | } 29 | 30 | func getParams(s string) (p rest.Params, err error) { 31 | p.Path = make(map[string]rest.PathParam) 32 | p.Query = make(map[string]rest.QueryParam) 33 | 34 | u, err := url.Parse(s) 35 | if err != nil { 36 | return 37 | } 38 | 39 | // Path. 40 | s = u.Path 41 | s = strings.TrimSuffix(s, "/") 42 | s = strings.TrimPrefix(s, "/") 43 | segments := strings.Split(s, "/") 44 | for _, segment := range segments { 45 | name, pattern, ok := getPlaceholder(segment) 46 | if !ok { 47 | continue 48 | } 49 | p.Path[name] = rest.PathParam{ 50 | Regexp: pattern, 51 | } 52 | } 53 | 54 | // Query. 55 | q := u.Query() 56 | for k := range q { 57 | name, _, ok := getPlaceholder(q.Get(k)) 58 | if !ok { 59 | continue 60 | } 61 | p.Query[name] = rest.QueryParam{ 62 | Description: "", 63 | Required: false, 64 | AllowEmpty: false, 65 | } 66 | } 67 | 68 | return 69 | } 70 | 71 | func getPlaceholder(s string) (name string, pattern string, ok bool) { 72 | if !strings.HasPrefix(s, "{") || !strings.HasSuffix(s, "}") { 73 | return 74 | } 75 | parts := strings.SplitN(s[1:len(s)-1], ":", 2) 76 | name = parts[0] 77 | if len(parts) > 1 { 78 | pattern = parts[1] 79 | } 80 | return name, pattern, true 81 | } 82 | -------------------------------------------------------------------------------- /chiadapter/route_test.go: -------------------------------------------------------------------------------- 1 | package chiadapter_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/a-h/rest" 8 | "github.com/a-h/rest/chiadapter" 9 | "github.com/go-chi/chi/v5" 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | func TestMerge(t *testing.T) { 14 | // Arrange. 15 | pattern := `/organisation/{orgId:\d+}/user/{userId}/{role}` 16 | router := chi.NewRouter() 17 | router.Method(http.MethodGet, pattern, 18 | http.RedirectHandler("/elsewhere", http.StatusMovedPermanently)) 19 | api := rest.NewAPI("test") 20 | 21 | // Act. 22 | err := chiadapter.Merge(api, router) 23 | if err != nil { 24 | t.Fatalf("failed to merge: %v", err) 25 | } 26 | api.Get(pattern).HasPathParameter("role", rest.PathParam{ 27 | Description: "Role of the user", 28 | }) 29 | 30 | // Assert. 31 | expected := rest.Params{ 32 | Path: map[string]rest.PathParam{ 33 | "orgId": {Regexp: `\d+`}, 34 | "userId": {}, 35 | "role": {Description: "Role of the user"}, 36 | }, 37 | Query: make(map[string]rest.QueryParam), 38 | } 39 | if diff := cmp.Diff(expected, api.Get(pattern).Params); diff != "" { 40 | t.Error(diff) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /enums/enums.go: -------------------------------------------------------------------------------- 1 | package enums 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "go/ast" 7 | "go/constant" 8 | "go/token" 9 | "go/types" 10 | "reflect" 11 | "strconv" 12 | 13 | "golang.org/x/tools/go/packages" 14 | ) 15 | 16 | func Get(ty reflect.Type) ([]any, error) { 17 | var enum []any 18 | config := &packages.Config{ 19 | Mode: packages.NeedName | 20 | packages.NeedFiles | 21 | packages.NeedCompiledGoFiles | 22 | packages.NeedTypes | 23 | packages.NeedSyntax | 24 | packages.NeedTypesInfo, 25 | // Only look in test files if a test is in progress. 26 | Tests: flag.Lookup("test.v") != nil, 27 | } 28 | config.Fset = token.NewFileSet() 29 | pkgs, err := packages.Load(config, ty.PkgPath()) 30 | if err != nil { 31 | return nil, fmt.Errorf("could not load package %q", ty.PkgPath()) 32 | } 33 | for _, p := range pkgs { 34 | for _, syn := range p.Syntax { 35 | for _, d := range syn.Decls { 36 | if _, ok := d.(*ast.GenDecl); !ok { 37 | continue 38 | } 39 | for _, sp := range d.(*ast.GenDecl).Specs { 40 | v, ok := sp.(*ast.ValueSpec) 41 | if !ok { 42 | continue 43 | } 44 | for _, name := range v.Names { 45 | v, err := getConstantValue(ty, name, p) 46 | if err != nil { 47 | return nil, err 48 | } 49 | if v != nil { 50 | enum = append(enum, v) 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | return enum, nil 58 | } 59 | 60 | func getConstantValue(ty reflect.Type, name *ast.Ident, pkg *packages.Package) (any, error) { 61 | c, ok := pkg.TypesInfo.ObjectOf(name).(*types.Const) 62 | if !ok { 63 | return nil, nil 64 | } 65 | if c.Type().String() == ty.PkgPath()+"."+ty.Name() { 66 | if c.Val().Kind() == constant.String { 67 | return constant.StringVal(c.Val()), nil 68 | } 69 | if c.Val().Kind() == constant.Int { 70 | n, err := strconv.Atoi(c.Val().ExactString()) 71 | if err != nil { 72 | return nil, fmt.Errorf("could not parse enum %s value: %q", ty.Name(), c.Val().ExactString()) 73 | } 74 | return n, nil 75 | } 76 | return c.Val().ExactString(), nil 77 | } 78 | return nil, nil 79 | } 80 | -------------------------------------------------------------------------------- /enums/enums_test.go: -------------------------------------------------------------------------------- 1 | package enums 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | type stringEnum string 11 | 12 | const ( 13 | stringEnum1 stringEnum = "" 14 | stringEnum2 stringEnum = "1" 15 | stringEnum3 stringEnum = "2" 16 | incorrectType = "invalid" 17 | stringEnum4 stringEnum = "3" 18 | ) 19 | 20 | var notAConstEnum stringEnum = "invalid" 21 | 22 | const stringEnum5 stringEnum = "4" 23 | 24 | type intEnum int 25 | 26 | const ( 27 | intEnum1 intEnum = 0 28 | intEnum2 intEnum = 1 29 | intEnum3 intEnum = 2 30 | incorrectIntType = "invalid" 31 | intEnum4 intEnum = 3 32 | ) 33 | 34 | var notAConstIntEnum intEnum = 4 35 | 36 | const intEnum5 intEnum = 5 37 | 38 | type iotaIntEnum int 39 | 40 | const ( 41 | iotaIntEnum1 iotaIntEnum = iota 42 | iotaIntEnum2 43 | iotaIntEnum3 44 | ) 45 | 46 | func TestGet(t *testing.T) { 47 | tests := []struct { 48 | name string 49 | ty reflect.Type 50 | expected []any 51 | }{ 52 | { 53 | name: "string enums", 54 | ty: reflect.TypeOf(stringEnum1), 55 | expected: []any{ 56 | string(stringEnum1), 57 | string(stringEnum2), 58 | string(stringEnum3), 59 | string(stringEnum4), 60 | string(stringEnum5), 61 | }, 62 | }, 63 | { 64 | name: "int enums", 65 | ty: reflect.TypeOf(intEnum1), 66 | expected: []any{ 67 | int(intEnum1), 68 | int(intEnum2), 69 | int(intEnum3), 70 | int(intEnum4), 71 | int(intEnum5), 72 | }, 73 | }, 74 | { 75 | name: "iota int enums", 76 | ty: reflect.TypeOf(iotaIntEnum1), 77 | expected: []any{ 78 | int(iotaIntEnum1), 79 | int(iotaIntEnum2), 80 | int(iotaIntEnum3), 81 | }, 82 | }, 83 | } 84 | 85 | for _, tt := range tests { 86 | tt := tt 87 | t.Run(tt.name, func(t *testing.T) { 88 | vals, err := Get(tt.ty) 89 | if err != nil { 90 | t.Error(err) 91 | } 92 | if diff := cmp.Diff(tt.expected, vals); diff != "" { 93 | t.Error(diff) 94 | } 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /examples/chiexample/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/a-h/rest/examples/chiexample 2 | 3 | go 1.21 4 | 5 | toolchain go1.22.2 6 | 7 | replace github.com/a-h/rest v0.0.0 => ../../ 8 | 9 | require ( 10 | github.com/a-h/respond v0.0.2 11 | github.com/a-h/rest v0.0.0 12 | github.com/getkin/kin-openapi v0.124.0 13 | github.com/go-chi/chi/v5 v5.0.12 14 | ) 15 | 16 | require ( 17 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 18 | github.com/go-openapi/swag v0.23.0 // indirect 19 | github.com/invopop/yaml v0.2.0 // indirect 20 | github.com/josharian/intern v1.0.0 // indirect 21 | github.com/mailru/easyjson v0.7.7 // indirect 22 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 23 | github.com/perimeterx/marshmallow v1.1.5 // indirect 24 | golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect 25 | golang.org/x/mod v0.17.0 // indirect 26 | golang.org/x/sync v0.7.0 // indirect 27 | golang.org/x/tools v0.20.0 // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /examples/chiexample/go.sum: -------------------------------------------------------------------------------- 1 | github.com/a-h/respond v0.0.2 h1:mhBwB2XuM+34gfIFs9LuXGfCCbu00rvaCWpTVNHvkPU= 2 | github.com/a-h/respond v0.0.2/go.mod h1:k9UvuVDWmHAb91OsdrqG0xFv7X+HelBpfMJIn9xMYWM= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= 6 | github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= 7 | github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= 8 | github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 9 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 10 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 11 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 12 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 13 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 14 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 15 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 16 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 17 | github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= 18 | github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= 19 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 20 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 21 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 22 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 23 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 24 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 25 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 26 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 27 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 28 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 29 | github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 30 | github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 34 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 35 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 36 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 37 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 38 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 39 | golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= 40 | golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= 41 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 42 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 43 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 44 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 45 | golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= 46 | golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= 47 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 48 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 49 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 50 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 51 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 52 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 54 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 55 | -------------------------------------------------------------------------------- /examples/chiexample/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/a-h/respond" 9 | "github.com/a-h/rest" 10 | "github.com/a-h/rest/chiadapter" 11 | "github.com/a-h/rest/examples/chiexample/models" 12 | "github.com/a-h/rest/swaggerui" 13 | "github.com/getkin/kin-openapi/openapi3" 14 | "github.com/go-chi/chi/v5" 15 | ) 16 | 17 | func main() { 18 | // Define routes in any router. 19 | router := chi.NewRouter() 20 | 21 | router.Get("/topic/{id}", func(w http.ResponseWriter, r *http.Request) { 22 | resp := models.Topic{ 23 | Namespace: "example", 24 | Topic: "topic", 25 | Private: false, 26 | ViewCount: 412, 27 | } 28 | respond.WithJSON(w, resp, http.StatusOK) 29 | }) 30 | 31 | router.Get("/topics", func(w http.ResponseWriter, r *http.Request) { 32 | resp := models.TopicsGetResponse{ 33 | Topics: []models.TopicRecord{ 34 | { 35 | ID: "testId", 36 | Topic: models.Topic{ 37 | Namespace: "example", 38 | Topic: "topic", 39 | Private: false, 40 | ViewCount: 412, 41 | }, 42 | }, 43 | }, 44 | } 45 | respond.WithJSON(w, resp, http.StatusOK) 46 | }) 47 | 48 | router.Post("/topics", func(w http.ResponseWriter, r *http.Request) { 49 | resp := models.TopicsPostResponse{ID: "123"} 50 | respond.WithJSON(w, resp, http.StatusOK) 51 | }) 52 | 53 | // Create the API definition. 54 | api := rest.NewAPI("Messaging API") 55 | 56 | // Create the routes and parameters of the Router in the REST API definition with an 57 | // adapter, or do it manually. 58 | chiadapter.Merge(api, router) 59 | 60 | // Because this example is all in the main package, we can strip the `main_` namespace from 61 | // the types. 62 | api.StripPkgPaths = []string{"main", "github.com/a-h"} 63 | 64 | // It's possible to customise the OpenAPI schema for each type. 65 | api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { 66 | status := s.Properties["statusCode"] 67 | status.Value.WithMin(100).WithMax(600) 68 | }) 69 | 70 | // Document the routes. 71 | api.Get("/topic/{id}"). 72 | HasResponseModel(http.StatusOK, rest.ModelOf[models.TopicsGetResponse]()). 73 | HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) 74 | 75 | api.Get("/topics"). 76 | HasResponseModel(http.StatusOK, rest.ModelOf[models.TopicsGetResponse]()). 77 | HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) 78 | 79 | api.Post("/topics"). 80 | HasRequestModel(rest.ModelOf[models.TopicsPostRequest]()). 81 | HasResponseModel(http.StatusOK, rest.ModelOf[models.TopicsPostResponse]()). 82 | HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) 83 | 84 | // Create the spec. 85 | spec, err := api.Spec() 86 | if err != nil { 87 | log.Fatalf("failed to create spec: %v", err) 88 | } 89 | 90 | // Customise it. 91 | spec.Info.Version = "v1.0.0" 92 | spec.Info.Description = "Messages API" 93 | 94 | // Attach the UI handler. 95 | ui, err := swaggerui.New(spec) 96 | if err != nil { 97 | log.Fatalf("failed to create swagger UI handler: %v", err) 98 | } 99 | router.Handle("/swagger-ui*", ui) 100 | // And start listening. 101 | fmt.Println("Listening on :8080...") 102 | fmt.Println("Visit http://localhost:8080/swagger-ui to see API definitions") 103 | http.ListenAndServe(":8080", router) 104 | } 105 | -------------------------------------------------------------------------------- /examples/chiexample/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Topic struct { 4 | Namespace string `json:"namespace"` 5 | Topic string `json:"topic"` 6 | Private bool `json:"private"` 7 | ViewCount int64 `json:"viewCount"` 8 | } 9 | 10 | // TopicsPostRequest is the request to POST /topics. 11 | type TopicsPostRequest struct { 12 | Topic 13 | } 14 | 15 | type TopicsPostResponse struct { 16 | ID string `json:"id"` 17 | } 18 | 19 | // TopicsGetResponse is the response to GET /topics. 20 | type TopicsGetResponse struct { 21 | Topics []TopicRecord `json:"topics"` 22 | } 23 | 24 | type TopicRecord struct { 25 | // ID of the topic record. 26 | ID string `json:"id"` 27 | Topic 28 | } 29 | -------------------------------------------------------------------------------- /examples/offline/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/a-h/rest/examples/offline 2 | 3 | go 1.21 4 | 5 | toolchain go1.22.2 6 | 7 | replace github.com/a-h/rest v0.0.0 => ../../ 8 | 9 | require ( 10 | github.com/a-h/respond v0.0.2 11 | github.com/a-h/rest v0.0.0 12 | github.com/getkin/kin-openapi v0.124.0 13 | ) 14 | 15 | require ( 16 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 17 | github.com/go-openapi/swag v0.23.0 // indirect 18 | github.com/invopop/yaml v0.2.0 // indirect 19 | github.com/josharian/intern v1.0.0 // indirect 20 | github.com/mailru/easyjson v0.7.7 // indirect 21 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 22 | github.com/perimeterx/marshmallow v1.1.5 // indirect 23 | golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect 24 | golang.org/x/mod v0.17.0 // indirect 25 | golang.org/x/sync v0.7.0 // indirect 26 | golang.org/x/tools v0.20.0 // indirect 27 | gopkg.in/yaml.v3 v3.0.1 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /examples/offline/go.sum: -------------------------------------------------------------------------------- 1 | github.com/a-h/respond v0.0.2 h1:mhBwB2XuM+34gfIFs9LuXGfCCbu00rvaCWpTVNHvkPU= 2 | github.com/a-h/respond v0.0.2/go.mod h1:k9UvuVDWmHAb91OsdrqG0xFv7X+HelBpfMJIn9xMYWM= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= 6 | github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= 7 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 8 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 9 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 10 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 11 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 12 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 13 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 14 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= 16 | github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= 17 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 18 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 19 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 20 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 21 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 22 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 23 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 24 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 25 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 26 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 27 | github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 28 | github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 32 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 33 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 34 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 35 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 36 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 37 | golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= 38 | golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= 39 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 40 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 41 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 42 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 43 | golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= 44 | golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 47 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 48 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 49 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 50 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 51 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 52 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | -------------------------------------------------------------------------------- /examples/offline/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/a-h/respond" 10 | "github.com/a-h/rest" 11 | "github.com/a-h/rest/examples/offline/models" 12 | "github.com/getkin/kin-openapi/openapi3" 13 | ) 14 | 15 | func main() { 16 | // Configure the models. 17 | api := rest.NewAPI("messages") 18 | api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} 19 | 20 | api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { 21 | status := s.Properties["statusCode"] 22 | status.Value.WithMin(100).WithMax(600) 23 | }) 24 | 25 | api.Get("/topic/{id}"). 26 | HasPathParameter("id", rest.PathParam{ 27 | Description: "id of the topic", 28 | Regexp: `\d+`, 29 | }). 30 | HasResponseModel(http.StatusOK, rest.ModelOf[models.Topic]()). 31 | HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()). 32 | HasTags([]string{"Topic"}). 33 | HasDescription("Get one topic by id"). 34 | HasOperationID("getOneTopic") 35 | 36 | // Create the specification. 37 | spec, err := api.Spec() 38 | if err != nil { 39 | log.Fatalf("failed to create spec: %v", err) 40 | } 41 | 42 | // Write to stdout. 43 | enc := json.NewEncoder(os.Stdout) 44 | enc.SetIndent("", " ") 45 | enc.Encode(spec) 46 | } 47 | -------------------------------------------------------------------------------- /examples/offline/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Topic of a thread. 4 | type Topic struct { 5 | Namespace string `json:"namespace"` 6 | Topic string `json:"topic"` 7 | Private bool `json:"private"` 8 | ViewCount int64 `json:"viewCount"` 9 | } 10 | -------------------------------------------------------------------------------- /examples/stdlib/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/a-h/rest/examples/stdlib 2 | 3 | go 1.21 4 | 5 | toolchain go1.22.2 6 | 7 | replace github.com/a-h/rest v0.0.0 => ../../ 8 | 9 | require ( 10 | github.com/a-h/respond v0.0.2 11 | github.com/a-h/rest v0.0.0 12 | github.com/getkin/kin-openapi v0.124.0 13 | ) 14 | 15 | require ( 16 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 17 | github.com/go-openapi/swag v0.23.0 // indirect 18 | github.com/invopop/yaml v0.2.0 // indirect 19 | github.com/josharian/intern v1.0.0 // indirect 20 | github.com/mailru/easyjson v0.7.7 // indirect 21 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 22 | github.com/perimeterx/marshmallow v1.1.5 // indirect 23 | golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect 24 | golang.org/x/mod v0.17.0 // indirect 25 | golang.org/x/sync v0.7.0 // indirect 26 | golang.org/x/tools v0.20.0 // indirect 27 | gopkg.in/yaml.v3 v3.0.1 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /examples/stdlib/go.sum: -------------------------------------------------------------------------------- 1 | github.com/a-h/respond v0.0.2 h1:mhBwB2XuM+34gfIFs9LuXGfCCbu00rvaCWpTVNHvkPU= 2 | github.com/a-h/respond v0.0.2/go.mod h1:k9UvuVDWmHAb91OsdrqG0xFv7X+HelBpfMJIn9xMYWM= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= 6 | github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= 7 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 8 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 9 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 10 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 11 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 12 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 13 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 14 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= 16 | github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= 17 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 18 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 19 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 20 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 21 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 22 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 23 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 24 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 25 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 26 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 27 | github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 28 | github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 32 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 33 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 34 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 35 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 36 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 37 | golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= 38 | golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= 39 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 40 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 41 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 42 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 43 | golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= 44 | golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 47 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 48 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 49 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 50 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 51 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 52 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | -------------------------------------------------------------------------------- /examples/stdlib/handlers/topic/post/handler.go: -------------------------------------------------------------------------------- 1 | package post 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/a-h/rest/examples/stdlib/models" 7 | ) 8 | 9 | type TopicPostRequest struct { 10 | models.Topic 11 | } 12 | 13 | type TopicPostResponse struct { 14 | OK bool `json:"ok"` 15 | } 16 | 17 | type Handler struct { 18 | } 19 | 20 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 21 | } 22 | -------------------------------------------------------------------------------- /examples/stdlib/handlers/topics/get/handler.go: -------------------------------------------------------------------------------- 1 | package get 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/a-h/rest/examples/stdlib/models" 7 | ) 8 | 9 | type TopicsGetResponse struct { 10 | Topics []models.Topic `json:"topics"` 11 | } 12 | 13 | type Handler struct { 14 | } 15 | 16 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 17 | } 18 | -------------------------------------------------------------------------------- /examples/stdlib/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/a-h/respond" 9 | "github.com/a-h/rest" 10 | "github.com/a-h/rest/examples/stdlib/handlers/topic/post" 11 | "github.com/a-h/rest/examples/stdlib/handlers/topics/get" 12 | "github.com/a-h/rest/swaggerui" 13 | "github.com/getkin/kin-openapi/openapi3" 14 | ) 15 | 16 | func main() { 17 | // Create standard routes. 18 | router := http.NewServeMux() 19 | router.Handle("/topics", &get.Handler{}) 20 | router.Handle("/topic", &post.Handler{}) 21 | 22 | api := rest.NewAPI("messages") 23 | api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} 24 | 25 | // It's possible to customise the OpenAPI schema for each type. 26 | // You can use helper functions, or write your own function that works 27 | // directly on the openapi3.Schema type. 28 | api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { 29 | status := s.Properties["statusCode"] 30 | status.Value.WithMin(100).WithMax(600) 31 | }) 32 | 33 | api.Get("/topics"). 34 | HasResponseModel(http.StatusOK, rest.ModelOf[get.TopicsGetResponse]()). 35 | HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) 36 | 37 | api.Post("/topic"). 38 | HasRequestModel(rest.ModelOf[post.TopicPostRequest]()). 39 | HasResponseModel(http.StatusOK, rest.ModelOf[post.TopicPostResponse]()). 40 | HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) 41 | 42 | // Create the spec. 43 | spec, err := api.Spec() 44 | if err != nil { 45 | log.Fatalf("failed to create spec: %v", err) 46 | } 47 | 48 | // Apply any global customisation. 49 | spec.Info.Version = "v1.0.0." 50 | spec.Info.Description = "Messages API" 51 | 52 | // Attach the Swagger UI handler to your router. 53 | ui, err := swaggerui.New(spec) 54 | if err != nil { 55 | log.Fatalf("failed to create swagger UI handler: %v", err) 56 | } 57 | router.Handle("/swagger-ui", ui) 58 | router.Handle("/swagger-ui/", ui) 59 | 60 | // And start listening. 61 | fmt.Println("Listening on :8080...") 62 | fmt.Println("Visit http://localhost:8080/swagger-ui to see API definitions") 63 | fmt.Println("Listening on :8080...") 64 | http.ListenAndServe(":8080", router) 65 | } 66 | -------------------------------------------------------------------------------- /examples/stdlib/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Topic of a thread. 4 | type Topic struct { 5 | Namespace string `json:"namespace"` 6 | Topic string `json:"topic"` 7 | Private bool `json:"private"` 8 | ViewCount int64 `json:"viewCount"` 9 | } 10 | -------------------------------------------------------------------------------- /getcomments/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | 10 | "github.com/a-h/rest/getcomments/parser" 11 | ) 12 | 13 | var flagPackage = flag.String("package", "", "The package to retrieve comments from, e.g. github.com/a-h/rest/getcomments/example") 14 | 15 | func main() { 16 | flag.Parse() 17 | if *flagPackage == "" { 18 | flag.Usage() 19 | os.Exit(0) 20 | } 21 | m, err := parser.Get(*flagPackage) 22 | if err != nil { 23 | log.Fatalf("failed to parse: %v", err) 24 | } 25 | enc := json.NewEncoder(os.Stdout) 26 | enc.SetIndent("", " ") 27 | err = enc.Encode(m) 28 | if err != nil { 29 | fmt.Printf("error encoding: %v\n", err) 30 | os.Exit(1) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /getcomments/parser/README.md: -------------------------------------------------------------------------------- 1 | # Parser 2 | 3 | ## Tasks 4 | 5 | ### snapshot 6 | 7 | Snapshot all of the tests. 8 | 9 | ```sh 10 | ls -d tests/* | xargs -I '{}' go run snapshot/main.go -pkg="github.com/a-h/rest/getcomments/parser/{}" -op="./{}/snapshot.json" 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /getcomments/parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/types" 7 | "strings" 8 | 9 | "golang.org/x/tools/go/packages" 10 | ) 11 | 12 | func Get(packageName string) (m map[string]string, err error) { 13 | config := &packages.Config{ 14 | Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax, 15 | Tests: true, 16 | } 17 | pkgs, err := packages.Load(config, packageName) 18 | if err != nil { 19 | err = fmt.Errorf("error loading package %s: %w", packageName, err) 20 | return 21 | } 22 | 23 | // Add the comments to the definitions. 24 | m = make(map[string]string) 25 | for _, pkg := range pkgs { 26 | for _, file := range pkg.Syntax { 27 | processFile(packageName, pkg, file, m) 28 | } 29 | } 30 | return 31 | } 32 | 33 | func processFile(packageName string, pkg *packages.Package, file *ast.File, m map[string]string) { 34 | var lastComment string 35 | var typ string 36 | ast.Inspect(file, func(n ast.Node) bool { 37 | switch x := n.(type) { 38 | case *ast.TypeSpec: 39 | typ = x.Name.String() 40 | if !ast.IsExported(typ) { 41 | break 42 | } 43 | typeID := fmt.Sprintf("%s.%s", packageName, typ) 44 | if lastComment != "" { 45 | m[typeID] = lastComment 46 | } 47 | case *ast.GenDecl: 48 | lastComment = strings.TrimSpace(x.Doc.Text()) 49 | case *ast.ValueSpec: 50 | // Get comments on constants, since they may appear in string and integer enums. 51 | for _, name := range x.Names { 52 | c, isConstant := pkg.TypesInfo.ObjectOf(name).(*types.Const) 53 | if !isConstant { 54 | continue 55 | } 56 | typeID := fmt.Sprintf("%s.%s", packageName, c.Name()) 57 | comments := lastComment 58 | if strings.TrimSpace(x.Doc.Text()) != "" { 59 | comments = strings.TrimSpace(x.Doc.Text()) 60 | } 61 | if comments != "" { 62 | m[typeID] = comments 63 | } 64 | } 65 | case *ast.FuncDecl: 66 | // Skip functions, since they can't appear in schema. 67 | return false 68 | case *ast.Field: 69 | if typ == "" { 70 | break 71 | } 72 | if !ast.IsExported(typ) { 73 | break 74 | } 75 | fieldName := getFieldName(x) 76 | if !ast.IsExported(fieldName) { 77 | break 78 | } 79 | typeID := fmt.Sprintf("%s.%s.%s", packageName, typ, fieldName) 80 | comments := strings.TrimSpace(x.Doc.Text()) 81 | if comments != "" { 82 | m[typeID] = comments 83 | } 84 | } 85 | return true 86 | }) 87 | } 88 | 89 | func getFieldName(field *ast.Field) string { 90 | var names []string 91 | for _, name := range field.Names { 92 | names = append(names, name.Name) 93 | } 94 | return strings.Join(names, ".") 95 | } 96 | -------------------------------------------------------------------------------- /getcomments/parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/a-h/rest/getcomments/parser" 8 | "github.com/a-h/rest/getcomments/parser/tests/anonymous" 9 | "github.com/a-h/rest/getcomments/parser/tests/chans" 10 | "github.com/a-h/rest/getcomments/parser/tests/docs" 11 | "github.com/a-h/rest/getcomments/parser/tests/enum" 12 | "github.com/a-h/rest/getcomments/parser/tests/functions" 13 | "github.com/a-h/rest/getcomments/parser/tests/functiontypes" 14 | "github.com/a-h/rest/getcomments/parser/tests/pointers" 15 | "github.com/a-h/rest/getcomments/parser/tests/privatetypes" 16 | "github.com/a-h/rest/getcomments/parser/tests/publictypes" 17 | "github.com/google/go-cmp/cmp" 18 | ) 19 | 20 | func TestGet(t *testing.T) { 21 | tests := []struct { 22 | name string 23 | pkg string 24 | expected string 25 | }{ 26 | { 27 | name: "private structs are ignored", 28 | pkg: "github.com/a-h/rest/getcomments/parser/tests/privatetypes", 29 | expected: privatetypes.Expected, 30 | }, 31 | { 32 | name: "public structs are included", 33 | pkg: "github.com/a-h/rest/getcomments/parser/tests/publictypes", 34 | expected: publictypes.Expected, 35 | }, 36 | { 37 | name: "string and integer enums are supported", 38 | pkg: "github.com/a-h/rest/getcomments/parser/tests/enum", 39 | expected: enum.Expected, 40 | }, 41 | { 42 | name: "pointers to pointers become a single pointer", 43 | pkg: "github.com/a-h/rest/getcomments/parser/tests/pointers", 44 | expected: pointers.Expected, 45 | }, 46 | { 47 | name: "functions and method receivers are ignored", 48 | pkg: "github.com/a-h/rest/getcomments/parser/tests/functions", 49 | expected: functions.Expected, 50 | }, 51 | { 52 | name: "fields of type channel are ignored", 53 | pkg: "github.com/a-h/rest/getcomments/parser/tests/chans", 54 | expected: chans.Expected, 55 | }, 56 | { 57 | name: "anonymous structs are ignored", 58 | pkg: "github.com/a-h/rest/getcomments/parser/tests/anonymous", 59 | expected: anonymous.Expected, 60 | }, 61 | { 62 | name: "function fields and function types are ignored", 63 | pkg: "github.com/a-h/rest/getcomments/parser/tests/functiontypes", 64 | expected: functiontypes.Expected, 65 | }, 66 | { 67 | name: "stuct, field and constant comments are extracted", 68 | pkg: "github.com/a-h/rest/getcomments/parser/tests/docs", 69 | expected: docs.Expected, 70 | }, 71 | } 72 | 73 | for _, test := range tests { 74 | test := test 75 | t.Run(test.name, func(t *testing.T) { 76 | m, err := parser.Get(test.pkg) 77 | if err != nil { 78 | t.Fatalf("failed to get model %q: %v", test.pkg, err) 79 | } 80 | 81 | var expected map[string]string 82 | err = json.Unmarshal([]byte(test.expected), &expected) 83 | if err != nil { 84 | t.Fatalf("snapshot load failed: %v", err) 85 | } 86 | 87 | if diff := cmp.Diff(expected, m); diff != "" { 88 | t.Error(diff) 89 | } 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /getcomments/parser/snapshot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/a-h/rest/getcomments/parser" 10 | ) 11 | 12 | var flagPkg = flag.String("pkg", "", "Name of the package to process.") 13 | var flagOutput = flag.String("op", "", "Name of the file to write to.") 14 | 15 | func main() { 16 | flag.Parse() 17 | if *flagPkg == "" { 18 | fmt.Println("missing package name") 19 | os.Exit(1) 20 | } 21 | if *flagOutput == "" { 22 | fmt.Println("missing output name") 23 | os.Exit(1) 24 | } 25 | m, err := parser.Get(*flagPkg) 26 | if err != nil { 27 | fmt.Printf("failed to get model: %v", err) 28 | os.Exit(1) 29 | } 30 | f, err := os.Create(*flagOutput) 31 | if err != nil { 32 | fmt.Printf("error creating output file %q: %v\n", *flagOutput, err) 33 | os.Exit(1) 34 | } 35 | fmt.Printf("snapshotting package %q\n", *flagPkg) 36 | enc := json.NewEncoder(f) 37 | enc.SetIndent("", " ") 38 | err = enc.Encode(m) 39 | if err != nil { 40 | fmt.Printf("error encoding: %v\n", err) 41 | os.Exit(1) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /getcomments/parser/tests/anonymous/example.go: -------------------------------------------------------------------------------- 1 | package anonymous 2 | 3 | import _ "embed" 4 | 5 | //go:embed snapshot.json 6 | var Expected string 7 | 8 | // Data should be included. 9 | type Data struct { 10 | // A should be included. 11 | A struct { 12 | // B should be included. 13 | B string 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /getcomments/parser/tests/anonymous/snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "github.com/a-h/rest/getcomments/parser/tests/anonymous.Data": "Data should be included.", 3 | "github.com/a-h/rest/getcomments/parser/tests/anonymous.Data.A": "A should be included.", 4 | "github.com/a-h/rest/getcomments/parser/tests/anonymous.Data.B": "B should be included." 5 | } 6 | -------------------------------------------------------------------------------- /getcomments/parser/tests/chans/example.go: -------------------------------------------------------------------------------- 1 | package chans 2 | 3 | import _ "embed" 4 | 5 | //go:embed snapshot.json 6 | var Expected string 7 | 8 | // Data should be included. 9 | type Data struct { 10 | // AndIgnoreThisToo could be ignored, but isn't. 11 | AndIgnoreThisToo chan string 12 | // AndThisArrayOfChannels could also be ignored. 13 | AndThisArrayOfChannels []chan string 14 | // AndThisAlias could also be ignored. 15 | AndThisAlias ChanType 16 | } 17 | 18 | // IgnoreThisChannel could be ignored, since channels can't be part of schema. 19 | var IgnoreThisChannel chan string 20 | 21 | // ChanType could be ignored, since chanels can't be part of schema. 22 | type ChanType chan string 23 | -------------------------------------------------------------------------------- /getcomments/parser/tests/chans/snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "github.com/a-h/rest/getcomments/parser/tests/chans.ChanType": "ChanType could be ignored, since chanels can't be part of schema.", 3 | "github.com/a-h/rest/getcomments/parser/tests/chans.Data": "Data should be included.", 4 | "github.com/a-h/rest/getcomments/parser/tests/chans.Data.AndIgnoreThisToo": "AndIgnoreThisToo could be ignored, but isn't.", 5 | "github.com/a-h/rest/getcomments/parser/tests/chans.Data.AndThisAlias": "AndThisAlias could also be ignored.", 6 | "github.com/a-h/rest/getcomments/parser/tests/chans.Data.AndThisArrayOfChannels": "AndThisArrayOfChannels could also be ignored." 7 | } 8 | -------------------------------------------------------------------------------- /getcomments/parser/tests/docs/example.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed snapshot.json 6 | var Expected string 7 | 8 | // Struct documentation. 9 | type Data struct { 10 | // Field documentation. 11 | A string 12 | 13 | // Field documentation 14 | Cats CatType 15 | } 16 | 17 | // CatType is a type of cat. 18 | type CatType string 19 | 20 | // Looks grumpy, but is warm and caring. 21 | const CatTypePersian CatType = "persian" 22 | 23 | // Some say that these are relatively aggressive. 24 | const CatTypeManx CatType = "manx" 25 | 26 | const ( 27 | // CatTypeLion is a lion. 28 | CatTypeLion CatType = "lion" 29 | // The king of big cats. 30 | CatTypeTiger CatType = "tiger" 31 | ) 32 | -------------------------------------------------------------------------------- /getcomments/parser/tests/docs/snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "github.com/a-h/rest/getcomments/parser/tests/docs.CatType": "CatType is a type of cat.", 3 | "github.com/a-h/rest/getcomments/parser/tests/docs.CatTypeLion": "CatTypeLion is a lion.", 4 | "github.com/a-h/rest/getcomments/parser/tests/docs.CatTypeManx": "Some say that these are relatively aggressive.", 5 | "github.com/a-h/rest/getcomments/parser/tests/docs.CatTypePersian": "Looks grumpy, but is warm and caring.", 6 | "github.com/a-h/rest/getcomments/parser/tests/docs.CatTypeTiger": "The king of big cats.", 7 | "github.com/a-h/rest/getcomments/parser/tests/docs.Data": "Struct documentation.", 8 | "github.com/a-h/rest/getcomments/parser/tests/docs.Data.A": "Field documentation.", 9 | "github.com/a-h/rest/getcomments/parser/tests/docs.Data.Cats": "Field documentation" 10 | } 11 | -------------------------------------------------------------------------------- /getcomments/parser/tests/enum/example.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | import _ "embed" 4 | 5 | //go:embed snapshot.json 6 | var Expected string 7 | 8 | type Public struct { 9 | A StringEnum 10 | B IntEnum 11 | } 12 | 13 | // StringEnum should be included. 14 | type StringEnum string 15 | 16 | const ( 17 | // StringEnum comment. 18 | StringEnumA StringEnum = "A" 19 | StringEnumB StringEnum = "B" 20 | StringEnumC StringEnum = "C" 21 | ) 22 | 23 | // IntEnum should be included. 24 | type IntEnum int 25 | 26 | const ( 27 | // IntEnum0 should be included. 28 | IntEnum0 IntEnum = iota 29 | IntEnum1 30 | IntEnum2 31 | ) 32 | -------------------------------------------------------------------------------- /getcomments/parser/tests/enum/snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "github.com/a-h/rest/getcomments/parser/tests/enum.IntEnum": "IntEnum should be included.", 3 | "github.com/a-h/rest/getcomments/parser/tests/enum.IntEnum0": "IntEnum0 should be included.", 4 | "github.com/a-h/rest/getcomments/parser/tests/enum.StringEnum": "StringEnum should be included.", 5 | "github.com/a-h/rest/getcomments/parser/tests/enum.StringEnumA": "StringEnum comment." 6 | } 7 | -------------------------------------------------------------------------------- /getcomments/parser/tests/functions/example.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import _ "embed" 4 | 5 | //go:embed snapshot.json 6 | var Expected string 7 | 8 | // ThisShouldBeIgnored because a function can't be part of schema. 9 | func ThisShouldBeIgnored() { 10 | } 11 | 12 | // asShouldThis should be ignored, because it's not exported too. 13 | func asShouldThis() { 14 | } 15 | 16 | // Data should be included. 17 | type Data struct { 18 | // A should also be included. 19 | A string 20 | } 21 | 22 | // IgnoreMe because I'm a method on a type. 23 | func (d Data) IgnoreMe() { 24 | } 25 | 26 | func (d Data) andMeToo() { 27 | } 28 | 29 | func (d *Data) Same() { 30 | } 31 | 32 | // DontIgnoreMe just because I'm further down the file. 33 | type DontIgnoreMe struct { 34 | Please string 35 | } 36 | -------------------------------------------------------------------------------- /getcomments/parser/tests/functions/snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "github.com/a-h/rest/getcomments/parser/tests/functions.Data": "Data should be included.", 3 | "github.com/a-h/rest/getcomments/parser/tests/functions.Data.A": "A should also be included.", 4 | "github.com/a-h/rest/getcomments/parser/tests/functions.DontIgnoreMe": "DontIgnoreMe just because I'm further down the file." 5 | } 6 | -------------------------------------------------------------------------------- /getcomments/parser/tests/functiontypes/example.go: -------------------------------------------------------------------------------- 1 | package functiontypes 2 | 3 | import _ "embed" 4 | 5 | //go:embed snapshot.json 6 | var Expected string 7 | 8 | // Data should be documented. 9 | type Data struct { 10 | // The function here could be ignored. 11 | A func(test string) 12 | // As could this. 13 | B FuncType 14 | // Non exported fields are not included in the output. 15 | c func() 16 | } 17 | 18 | // No need to document this. 19 | type FuncType func() 20 | -------------------------------------------------------------------------------- /getcomments/parser/tests/functiontypes/snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "github.com/a-h/rest/getcomments/parser/tests/functiontypes.Data": "Data should be documented.", 3 | "github.com/a-h/rest/getcomments/parser/tests/functiontypes.Data.A": "The function here could be ignored.", 4 | "github.com/a-h/rest/getcomments/parser/tests/functiontypes.Data.B": "As could this.", 5 | "github.com/a-h/rest/getcomments/parser/tests/functiontypes.FuncType": "No need to document this." 6 | } 7 | -------------------------------------------------------------------------------- /getcomments/parser/tests/generics/example.go: -------------------------------------------------------------------------------- 1 | package generics 2 | 3 | import _ "embed" 4 | 5 | //go:embed snapshot.json 6 | var Expected string 7 | 8 | // Data should be included. 9 | type Data struct { 10 | // AllowThis should be included. 11 | AllowThis string 12 | // String should be included. 13 | String DataOfT[string] 14 | // Int should be included. 15 | Int DataOfT[int] 16 | } 17 | 18 | // DataOfT is included in the output. 19 | type DataOfT[T string | int] struct { 20 | Field T 21 | } 22 | -------------------------------------------------------------------------------- /getcomments/parser/tests/generics/snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "github.com/a-h/rest/getcomments/parser/tests/generics.Data": "Data should be included.", 3 | "github.com/a-h/rest/getcomments/parser/tests/generics.Data.AllowThis": "AllowThis should be included.", 4 | "github.com/a-h/rest/getcomments/parser/tests/generics.Data.Int": "Int should be included.", 5 | "github.com/a-h/rest/getcomments/parser/tests/generics.Data.String": "String should be included.", 6 | "github.com/a-h/rest/getcomments/parser/tests/generics.DataOfT": "DataOfT is included in the output." 7 | } 8 | -------------------------------------------------------------------------------- /getcomments/parser/tests/maps/example.go: -------------------------------------------------------------------------------- 1 | package unsupported 2 | 3 | // Type should be included. 4 | type Type struct { 5 | // MapOfStringToString should be included. 6 | MapOfStringToString map[string]string 7 | // MapOfMapsToMaps should be included. 8 | MapOfMapsToMaps map[string]map[string]string 9 | // MapOfMapValue should be included. 10 | MapOfMapValue map[string]MapValue 11 | } 12 | 13 | // MapValue should be included. 14 | type MapValue struct { 15 | // FieldA should be included. 16 | FieldA string 17 | } 18 | -------------------------------------------------------------------------------- /getcomments/parser/tests/maps/snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "github.com/a-h/rest/getcomments/parser/tests/maps.MapValue": "MapValue should be included.", 3 | "github.com/a-h/rest/getcomments/parser/tests/maps.MapValue.FieldA": "FieldA should be included.", 4 | "github.com/a-h/rest/getcomments/parser/tests/maps.Type": "Type should be included.", 5 | "github.com/a-h/rest/getcomments/parser/tests/maps.Type.MapOfMapValue": "MapOfMapValue should be included.", 6 | "github.com/a-h/rest/getcomments/parser/tests/maps.Type.MapOfMapsToMaps": "MapOfMapsToMaps should be included.", 7 | "github.com/a-h/rest/getcomments/parser/tests/maps.Type.MapOfStringToString": "MapOfStringToString should be included." 8 | } 9 | -------------------------------------------------------------------------------- /getcomments/parser/tests/pointers/example.go: -------------------------------------------------------------------------------- 1 | package pointers 2 | 3 | import _ "embed" 4 | 5 | //go:embed snapshot.json 6 | var Expected string 7 | 8 | // Public should be included. 9 | type Public struct { 10 | // A pointer to a pointer should be included. 11 | A **string 12 | } 13 | -------------------------------------------------------------------------------- /getcomments/parser/tests/pointers/snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "github.com/a-h/rest/getcomments/parser/tests/pointers.Public": "Public should be included.", 3 | "github.com/a-h/rest/getcomments/parser/tests/pointers.Public.A": "A pointer to a pointer should be included." 4 | } 5 | -------------------------------------------------------------------------------- /getcomments/parser/tests/privatetypes/example.go: -------------------------------------------------------------------------------- 1 | package privatetypes 2 | 3 | import _ "embed" 4 | 5 | //go:embed snapshot.json 6 | var Expected string 7 | 8 | // private types should not be included. 9 | type private struct { 10 | // A public field on a private type should not be included. 11 | A string 12 | B string 13 | } 14 | -------------------------------------------------------------------------------- /getcomments/parser/tests/privatetypes/snapshot.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /getcomments/parser/tests/publictypes/example.go: -------------------------------------------------------------------------------- 1 | package publictypes 2 | 3 | import _ "embed" 4 | 5 | //go:embed snapshot.json 6 | var Expected string 7 | 8 | // Public types should be included. 9 | type Public struct { 10 | // A public field on a public type should be included. 11 | A string 12 | B string 13 | // c should be skipped. 14 | c string 15 | } 16 | -------------------------------------------------------------------------------- /getcomments/parser/tests/publictypes/snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "github.com/a-h/rest/getcomments/parser/tests/publictypes.Public": "Public types should be included.", 3 | "github.com/a-h/rest/getcomments/parser/tests/publictypes.Public.A": "A public field on a public type should be included." 4 | } 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/a-h/rest 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/getkin/kin-openapi v0.124.0 7 | github.com/go-chi/chi/v5 v5.0.12 8 | github.com/google/go-cmp v0.5.9 9 | golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 10 | golang.org/x/tools v0.20.0 11 | gopkg.in/yaml.v2 v2.4.0 12 | ) 13 | 14 | require ( 15 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 16 | github.com/go-openapi/swag v0.23.0 // indirect 17 | github.com/invopop/yaml v0.3.1 // indirect 18 | github.com/josharian/intern v1.0.0 // indirect 19 | github.com/mailru/easyjson v0.7.7 // indirect 20 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 21 | github.com/perimeterx/marshmallow v1.1.5 // indirect 22 | golang.org/x/mod v0.17.0 // indirect 23 | golang.org/x/sync v0.7.0 // indirect 24 | gopkg.in/yaml.v3 v3.0.1 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= 4 | github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= 5 | github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= 6 | github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 7 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 8 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 9 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 10 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 11 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 12 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 13 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 14 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= 16 | github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= 17 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 18 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 19 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 20 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 21 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 22 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 23 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 24 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 25 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 26 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 27 | github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 28 | github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 32 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 33 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 34 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 35 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 36 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 37 | golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= 38 | golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= 39 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 40 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 41 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 42 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 43 | golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= 44 | golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 47 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 48 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 49 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 50 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 51 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 52 | -------------------------------------------------------------------------------- /schema.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "slices" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/a-h/rest/enums" 11 | "github.com/a-h/rest/getcomments/parser" 12 | "github.com/getkin/kin-openapi/openapi3" 13 | "golang.org/x/exp/constraints" 14 | ) 15 | 16 | func newSpec(name string) *openapi3.T { 17 | return &openapi3.T{ 18 | OpenAPI: "3.0.0", 19 | Info: &openapi3.Info{ 20 | Title: name, 21 | Version: "0.0.0", 22 | Extensions: map[string]interface{}{}, 23 | }, 24 | Components: &openapi3.Components{ 25 | Schemas: make(openapi3.Schemas), 26 | Extensions: map[string]interface{}{}, 27 | }, 28 | Paths: &openapi3.Paths{}, 29 | Extensions: map[string]interface{}{}, 30 | } 31 | } 32 | 33 | func getSortedKeys[V any](m map[string]V) (op []string) { 34 | for k := range m { 35 | op = append(op, k) 36 | } 37 | sort.Slice(op, func(i, j int) bool { 38 | return op[i] < op[j] 39 | }) 40 | return op 41 | } 42 | 43 | func newPrimitiveSchema(paramType PrimitiveType) *openapi3.Schema { 44 | switch paramType { 45 | case PrimitiveTypeString: 46 | return openapi3.NewStringSchema() 47 | case PrimitiveTypeBool: 48 | return openapi3.NewBoolSchema() 49 | case PrimitiveTypeInteger: 50 | return openapi3.NewIntegerSchema() 51 | case PrimitiveTypeFloat64: 52 | return openapi3.NewFloat64Schema() 53 | case "": 54 | return openapi3.NewStringSchema() 55 | default: 56 | return &openapi3.Schema{ 57 | Type: &openapi3.Types{string(paramType)}, 58 | } 59 | } 60 | } 61 | 62 | func (api *API) createOpenAPI() (spec *openapi3.T, err error) { 63 | spec = newSpec(api.Name) 64 | // Add all the routes. 65 | for pattern, methodToRoute := range api.Routes { 66 | path := &openapi3.PathItem{} 67 | for method, route := range methodToRoute { 68 | op := &openapi3.Operation{} 69 | 70 | // Add the query params. 71 | for _, k := range getSortedKeys(route.Params.Query) { 72 | v := route.Params.Query[k] 73 | 74 | ps := newPrimitiveSchema(v.Type). 75 | WithPattern(v.Regexp) 76 | queryParam := openapi3.NewQueryParameter(k). 77 | WithDescription(v.Description). 78 | WithSchema(ps) 79 | queryParam.Required = v.Required 80 | queryParam.AllowEmptyValue = v.AllowEmpty 81 | 82 | // Apply schema customisation. 83 | if v.ApplyCustomSchema != nil { 84 | v.ApplyCustomSchema(queryParam) 85 | } 86 | 87 | op.AddParameter(queryParam) 88 | } 89 | 90 | // Add the route params. 91 | for _, k := range getSortedKeys(route.Params.Path) { 92 | v := route.Params.Path[k] 93 | 94 | ps := newPrimitiveSchema(v.Type). 95 | WithPattern(v.Regexp) 96 | pathParam := openapi3.NewPathParameter(k). 97 | WithDescription(v.Description). 98 | WithSchema(ps) 99 | 100 | // Apply schema customisation. 101 | if v.ApplyCustomSchema != nil { 102 | v.ApplyCustomSchema(pathParam) 103 | } 104 | 105 | op.AddParameter(pathParam) 106 | } 107 | 108 | // Handle request types. 109 | if route.Models.Request.Type != nil { 110 | name, schema, err := api.RegisterModel(route.Models.Request) 111 | if err != nil { 112 | return spec, err 113 | } 114 | op.RequestBody = &openapi3.RequestBodyRef{ 115 | Value: openapi3.NewRequestBody().WithContent(map[string]*openapi3.MediaType{ 116 | "application/json": { 117 | Schema: getSchemaReferenceOrValue(name, schema), 118 | }, 119 | }), 120 | } 121 | } 122 | 123 | // Handle response types. 124 | for status, model := range route.Models.Responses { 125 | name, schema, err := api.RegisterModel(model) 126 | if err != nil { 127 | return spec, err 128 | } 129 | resp := openapi3.NewResponse(). 130 | WithDescription(""). 131 | WithContent(map[string]*openapi3.MediaType{ 132 | "application/json": { 133 | Schema: getSchemaReferenceOrValue(name, schema), 134 | }, 135 | }) 136 | op.AddResponse(status, resp) 137 | } 138 | 139 | // Handle tags. 140 | op.Tags = append(op.Tags, route.Tags...) 141 | 142 | // Handle OperationID. 143 | op.OperationID = route.OperationID 144 | 145 | // Handle description. 146 | op.Description = route.Description 147 | 148 | // Register the method. 149 | path.SetOperation(string(method), op) 150 | } 151 | 152 | // Populate the OpenAPI schemas from the models. 153 | for name, schema := range api.models { 154 | spec.Components.Schemas[name] = openapi3.NewSchemaRef("", schema) 155 | } 156 | 157 | spec.Paths.Set(string(pattern), path) 158 | } 159 | 160 | loader := openapi3.NewLoader() 161 | if err = loader.ResolveRefsIn(spec, nil); err != nil { 162 | return spec, fmt.Errorf("failed to resolve, due to external references: %w", err) 163 | } 164 | if err = spec.Validate(loader.Context); err != nil { 165 | return spec, fmt.Errorf("failed validation: %w", err) 166 | } 167 | 168 | return spec, err 169 | } 170 | 171 | func (api *API) getModelName(t reflect.Type) string { 172 | pkgPath, typeName := t.PkgPath(), t.Name() 173 | if t.Kind() == reflect.Pointer { 174 | pkgPath = t.Elem().PkgPath() 175 | typeName = t.Elem().Name() + "Ptr" 176 | } 177 | if t.Kind() == reflect.Map { 178 | typeName = fmt.Sprintf("map[%s]%s", t.Key().Name(), t.Elem().Name()) 179 | } 180 | schemaName := api.normalizeTypeName(pkgPath, typeName) 181 | if typeName == "" { 182 | schemaName = fmt.Sprintf("AnonymousType%d", len(api.models)) 183 | } 184 | return schemaName 185 | } 186 | 187 | func getSchemaReferenceOrValue(name string, schema *openapi3.Schema) *openapi3.SchemaRef { 188 | if shouldBeReferenced(schema) { 189 | return openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%s", name), nil) 190 | } 191 | return openapi3.NewSchemaRef("", schema) 192 | } 193 | 194 | // ModelOpts defines options that can be set when registering a model. 195 | type ModelOpts func(s *openapi3.Schema) 196 | 197 | // WithNullable sets the nullable field to true. 198 | func WithNullable() ModelOpts { 199 | return func(s *openapi3.Schema) { 200 | s.Nullable = true 201 | } 202 | } 203 | 204 | // WithDescription sets the description field on the schema. 205 | func WithDescription(desc string) ModelOpts { 206 | return func(s *openapi3.Schema) { 207 | s.Description = desc 208 | } 209 | } 210 | 211 | // WithEnumValues sets the property to be an enum value with the specific values. 212 | func WithEnumValues[T ~string | constraints.Integer](values ...T) ModelOpts { 213 | return func(s *openapi3.Schema) { 214 | if len(values) == 0 { 215 | return 216 | } 217 | s.Type = &openapi3.Types{openapi3.TypeString} 218 | if reflect.TypeOf(values[0]).Kind() != reflect.String { 219 | s.Type = &openapi3.Types{openapi3.TypeInteger} 220 | } 221 | for _, v := range values { 222 | s.Enum = append(s.Enum, v) 223 | } 224 | } 225 | } 226 | 227 | // WithEnumConstants sets the property to be an enum containing the values of the type found in the package. 228 | func WithEnumConstants[T ~string | constraints.Integer]() ModelOpts { 229 | return func(s *openapi3.Schema) { 230 | var t T 231 | ty := reflect.TypeOf(t) 232 | s.Type = &openapi3.Types{openapi3.TypeString} 233 | if ty.Kind() != reflect.String { 234 | s.Type = &openapi3.Types{openapi3.TypeInteger} 235 | } 236 | enum, err := enums.Get(ty) 237 | if err != nil { 238 | panic(err) 239 | } 240 | s.Enum = enum 241 | } 242 | } 243 | 244 | func isFieldRequired(isPointer, hasOmitEmpty bool) bool { 245 | return !(isPointer || hasOmitEmpty) 246 | } 247 | 248 | func isMarkedAsDeprecated(comment string) bool { 249 | // A field is only marked as deprecated if a paragraph (line) begins with Deprecated. 250 | // https://github.com/golang/go/wiki/Deprecated 251 | for _, line := range strings.Split(comment, "\n") { 252 | if strings.HasPrefix(strings.TrimSpace(line), "Deprecated:") { 253 | return true 254 | } 255 | } 256 | return false 257 | } 258 | 259 | // RegisterModel allows a model to be registered manually so that additional configuration can be applied. 260 | // The schema returned can be modified as required. 261 | func (api *API) RegisterModel(model Model, opts ...ModelOpts) (name string, schema *openapi3.Schema, err error) { 262 | // Get the name. 263 | t := model.Type 264 | name = api.getModelName(t) 265 | 266 | // If we've already got the schema, return it. 267 | var ok bool 268 | if schema, ok = api.models[name]; ok { 269 | return name, schema, nil 270 | } 271 | 272 | // It's known, but not in the schemaset yet. 273 | if knownSchema, ok := api.KnownTypes[t]; ok { 274 | // Objects, enums, need to be references, so add it into the 275 | // list. 276 | if shouldBeReferenced(&knownSchema) { 277 | api.models[name] = &knownSchema 278 | } 279 | return name, &knownSchema, nil 280 | } 281 | 282 | var elementName string 283 | var elementSchema *openapi3.Schema 284 | switch t.Kind() { 285 | case reflect.Slice, reflect.Array: 286 | elementName, elementSchema, err = api.RegisterModel(modelFromType(t.Elem())) 287 | if err != nil { 288 | return name, schema, fmt.Errorf("error getting schema of slice element %v: %w", t.Elem(), err) 289 | } 290 | schema = openapi3.NewArraySchema().WithNullable() // Arrays are always nilable in Go. 291 | schema.Items = getSchemaReferenceOrValue(elementName, elementSchema) 292 | case reflect.String: 293 | schema = openapi3.NewStringSchema() 294 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 295 | schema = openapi3.NewIntegerSchema() 296 | case reflect.Float64, reflect.Float32: 297 | schema = openapi3.NewFloat64Schema() 298 | case reflect.Bool: 299 | schema = openapi3.NewBoolSchema() 300 | case reflect.Pointer: 301 | name, schema, err = api.RegisterModel(modelFromType(t.Elem()), WithNullable()) 302 | case reflect.Map: 303 | // Check that the key is a string. 304 | if t.Key().Kind() != reflect.String { 305 | return name, schema, fmt.Errorf("maps must have a string key, but this map is of type %q", t.Key().String()) 306 | } 307 | 308 | // Get the element schema. 309 | elementName, elementSchema, err = api.RegisterModel(modelFromType(t.Elem())) 310 | if err != nil { 311 | return name, schema, fmt.Errorf("error getting schema of map value element %v: %w", t.Elem(), err) 312 | } 313 | schema = openapi3.NewObjectSchema().WithNullable() 314 | schema.AdditionalProperties.Schema = getSchemaReferenceOrValue(elementName, elementSchema) 315 | case reflect.Struct: 316 | schema = openapi3.NewObjectSchema() 317 | if schema.Description, schema.Deprecated, err = api.getTypeComment(t.PkgPath(), t.Name()); err != nil { 318 | return name, schema, fmt.Errorf("failed to get comments for type %q: %w", name, err) 319 | } 320 | schema.Properties = make(openapi3.Schemas) 321 | for i := 0; i < t.NumField(); i++ { 322 | f := t.Field(i) 323 | if !f.IsExported() { 324 | continue 325 | } 326 | // Get JSON fieldName. 327 | jsonTags := strings.Split(f.Tag.Get("json"), ",") 328 | fieldName := jsonTags[0] 329 | if fieldName == "" { 330 | fieldName = f.Name 331 | } 332 | // If the model doesn't exist. 333 | _, alreadyExists := api.models[api.getModelName(f.Type)] 334 | fieldSchemaName, fieldSchema, err := api.RegisterModel(modelFromType(f.Type)) 335 | if err != nil { 336 | return name, schema, fmt.Errorf("error getting schema for type %q, field %q, failed to get schema for embedded type %q: %w", t, fieldName, f.Type, err) 337 | } 338 | if f.Anonymous { 339 | // It's an anonymous type, no need for a reference to it, 340 | // since we're copying the fields. 341 | if !alreadyExists { 342 | delete(api.models, fieldSchemaName) 343 | } 344 | // Add all embedded fields to this type. 345 | for name, ref := range fieldSchema.Properties { 346 | schema.Properties[name] = ref 347 | } 348 | schema.Required = append(schema.Required, fieldSchema.Required...) 349 | continue 350 | } 351 | ref := getSchemaReferenceOrValue(fieldSchemaName, fieldSchema) 352 | if ref.Value != nil { 353 | if ref.Value.Description, ref.Value.Deprecated, err = api.getTypeFieldComment(t.PkgPath(), t.Name(), f.Name); err != nil { 354 | return name, schema, fmt.Errorf("failed to get comments for field %q in type %q: %w", fieldName, name, err) 355 | } 356 | } 357 | schema.Properties[fieldName] = ref 358 | isPtr := f.Type.Kind() == reflect.Pointer 359 | hasOmitEmptySet := slices.Contains(jsonTags, "omitempty") 360 | if isFieldRequired(isPtr, hasOmitEmptySet) { 361 | schema.Required = append(schema.Required, fieldName) 362 | } 363 | } 364 | } 365 | 366 | if schema == nil { 367 | return name, schema, fmt.Errorf("unsupported type: %v/%v", t.PkgPath(), t.Name()) 368 | } 369 | 370 | // Apply global customisation. 371 | if api.ApplyCustomSchemaToType != nil { 372 | api.ApplyCustomSchemaToType(t, schema) 373 | } 374 | 375 | // Customise the model using its ApplyCustomSchema method. 376 | // This allows any type to customise its schema. 377 | model.ApplyCustomSchema(schema) 378 | 379 | for _, opt := range opts { 380 | opt(schema) 381 | } 382 | 383 | // After all processing, register the type if required. 384 | if shouldBeReferenced(schema) { 385 | api.models[name] = schema 386 | return 387 | } 388 | 389 | return 390 | } 391 | 392 | func (api *API) getCommentsForPackage(pkg string) (pkgComments map[string]string, err error) { 393 | if pkgComments, loaded := api.comments[pkg]; loaded { 394 | return pkgComments, nil 395 | } 396 | pkgComments, err = parser.Get(pkg) 397 | if err != nil { 398 | return 399 | } 400 | api.comments[pkg] = pkgComments 401 | return 402 | } 403 | 404 | func (api *API) getTypeComment(pkg string, name string) (comment string, deprecated bool, err error) { 405 | pkgComments, err := api.getCommentsForPackage(pkg) 406 | if err != nil { 407 | return 408 | } 409 | comment = pkgComments[pkg+"."+name] 410 | deprecated = isMarkedAsDeprecated(comment) 411 | return 412 | } 413 | 414 | func (api *API) getTypeFieldComment(pkg string, name string, field string) (comment string, deprecated bool, err error) { 415 | pkgComments, err := api.getCommentsForPackage(pkg) 416 | if err != nil { 417 | return 418 | } 419 | comment = pkgComments[pkg+"."+name+"."+field] 420 | deprecated = isMarkedAsDeprecated(comment) 421 | return 422 | } 423 | 424 | func shouldBeReferenced(schema *openapi3.Schema) bool { 425 | if schema.Type.Is(openapi3.TypeObject) && schema.AdditionalProperties.Schema == nil { 426 | return true 427 | } 428 | if len(schema.Enum) > 0 { 429 | return true 430 | } 431 | return false 432 | } 433 | 434 | var normalizer = strings.NewReplacer("/", "_", 435 | ".", "_", 436 | "[", "_", 437 | "]", "_") 438 | 439 | func (api *API) normalizeTypeName(pkgPath, name string) string { 440 | var omitPackage bool 441 | for _, pkg := range api.StripPkgPaths { 442 | if strings.HasPrefix(pkgPath, pkg) { 443 | omitPackage = true 444 | break 445 | } 446 | } 447 | if omitPackage || pkgPath == "" { 448 | return normalizer.Replace(name) 449 | } 450 | return normalizer.Replace(pkgPath + "/" + name) 451 | } 452 | -------------------------------------------------------------------------------- /schema_test.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "embed" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | _ "embed" 14 | 15 | "github.com/getkin/kin-openapi/openapi3" 16 | "github.com/google/go-cmp/cmp" 17 | "gopkg.in/yaml.v2" 18 | ) 19 | 20 | //go:embed tests/* 21 | var testFiles embed.FS 22 | 23 | type TestRequestType struct { 24 | IntField int 25 | } 26 | 27 | // TestResponseType description. 28 | type TestResponseType struct { 29 | // IntField description. 30 | IntField int 31 | } 32 | 33 | type AllBasicDataTypes struct { 34 | Int int 35 | Int8 int8 36 | Int16 int16 37 | Int32 int32 38 | Int64 int64 39 | Uint uint 40 | Uint8 uint8 41 | Uint16 uint16 42 | Uint32 uint32 43 | Uint64 uint64 44 | Uintptr uintptr 45 | Float32 float32 46 | Float64 float64 47 | Byte byte 48 | Rune rune 49 | String string 50 | Bool bool 51 | } 52 | 53 | type AllBasicDataTypesPointers struct { 54 | Int *int 55 | Int8 *int8 56 | Int16 *int16 57 | Int32 *int32 58 | Int64 *int64 59 | Uint *uint 60 | Uint8 *uint8 61 | Uint16 *uint16 62 | Uint32 *uint32 63 | Uint64 *uint64 64 | Uintptr *uintptr 65 | Float32 *float32 66 | Float64 *float64 67 | Byte *byte 68 | Rune *rune 69 | String *string 70 | Bool *bool 71 | } 72 | 73 | type OmitEmptyFields struct { 74 | A string 75 | B string `json:",omitempty"` 76 | C *string 77 | D *string `json:",omitempty"` 78 | } 79 | 80 | type EmbeddedStructA struct { 81 | A string 82 | } 83 | type EmbeddedStructB struct { 84 | B string 85 | OptionalB string `json:",omitempty"` 86 | PointerB *string 87 | OptionalPointerB *string `json:",omitempty"` 88 | } 89 | 90 | type WithEmbeddedStructs struct { 91 | EmbeddedStructA 92 | EmbeddedStructB 93 | C string 94 | } 95 | 96 | type WithNameStructTags struct { 97 | // FirstName of something. 98 | FirstName string `json:"firstName"` 99 | // LastName of something. 100 | LastName string 101 | // FullName of something. 102 | // Deprecated: Use FirstName and LastName 103 | FullName string 104 | // MiddleName of something. Deprecated: This deprecation flag is not valid so this field should 105 | // not be marked as deprecated. 106 | MiddleName string 107 | } 108 | 109 | type KnownTypes struct { 110 | Time time.Time `json:"time"` 111 | TimePtr *time.Time `json:"timePtr"` 112 | } 113 | 114 | type User struct { 115 | ID int `json:"id"` 116 | Name string `json:"name"` 117 | } 118 | 119 | type OK struct { 120 | OK bool `json:"ok"` 121 | } 122 | 123 | type StringEnum string 124 | 125 | const ( 126 | StringEnumA StringEnum = "A" 127 | StringEnumB StringEnum = "B" 128 | StringEnumC StringEnum = "B" 129 | ) 130 | 131 | type IntEnum int64 132 | 133 | const ( 134 | IntEnum1 IntEnum = 1 135 | IntEnum2 IntEnum = 2 136 | IntEnum3 IntEnum = 3 137 | ) 138 | 139 | type WithEnums struct { 140 | S StringEnum `json:"s"` 141 | SS []StringEnum `json:"ss"` 142 | I IntEnum `json:"i"` 143 | V string `json:"v"` 144 | } 145 | 146 | type Pence int64 147 | 148 | type WithMaps struct { 149 | Amounts map[string]Pence `json:"amounts"` 150 | } 151 | 152 | type MultipleDateFieldsWithComments struct { 153 | // DateField is a field containing a date 154 | DateField time.Time `json:"dateField"` 155 | // DateFieldA is another field containing a date 156 | DateFieldA time.Time `json:"dateFieldA"` 157 | } 158 | 159 | type StructWithCustomisation struct { 160 | A string `json:"a"` 161 | B FieldWithCustomisation `json:"b"` 162 | C *FieldWithCustomisation `json:"c"` 163 | } 164 | 165 | func (*StructWithCustomisation) ApplyCustomSchema(s *openapi3.Schema) { 166 | s.Properties["a"].Value.Description = "A string" 167 | s.Properties["a"].Value.Example = "test" 168 | s.Properties["b"].Value.Description = "A custom field" 169 | } 170 | 171 | type FieldWithCustomisation string 172 | 173 | func (*FieldWithCustomisation) ApplyCustomSchema(s *openapi3.Schema) { 174 | s.Format = "custom" 175 | s.Example = "model_field_customisation" 176 | } 177 | 178 | type StructWithTags struct { 179 | A string `json:"a" rest:"A is a string."` 180 | } 181 | 182 | func TestSchema(t *testing.T) { 183 | tests := []struct { 184 | name string 185 | opts []APIOpts 186 | setup func(api *API) error 187 | }{ 188 | { 189 | name: "test000.yaml", 190 | setup: func(api *API) error { return nil }, 191 | }, 192 | { 193 | name: "test001.yaml", 194 | setup: func(api *API) error { 195 | api.Post("/test"). 196 | HasRequestModel(ModelOf[TestRequestType]()). 197 | HasResponseModel(http.StatusOK, ModelOf[TestResponseType]()). 198 | HasDescription("Test request type description"). 199 | HasTags([]string{"TestRequest"}) 200 | return nil 201 | }, 202 | }, 203 | { 204 | name: "basic-data-types.yaml", 205 | setup: func(api *API) error { 206 | api.Post("/test"). 207 | HasRequestModel(ModelOf[AllBasicDataTypes]()). 208 | HasResponseModel(http.StatusOK, ModelOf[AllBasicDataTypes]()). 209 | HasOperationID("postAllBasicDataTypes"). 210 | HasTags([]string{"BasicData"}). 211 | HasDescription("Post all basic data types description") 212 | return nil 213 | }, 214 | }, 215 | { 216 | name: "basic-data-types-pointers.yaml", 217 | setup: func(api *API) error { 218 | api.Post("/test"). 219 | HasRequestModel(ModelOf[AllBasicDataTypesPointers]()). 220 | HasResponseModel(http.StatusOK, ModelOf[AllBasicDataTypesPointers]()) 221 | return nil 222 | }, 223 | }, 224 | { 225 | name: "omit-empty-fields.yaml", 226 | setup: func(api *API) error { 227 | api.Post("/test"). 228 | HasRequestModel(ModelOf[OmitEmptyFields]()). 229 | HasResponseModel(http.StatusOK, ModelOf[OmitEmptyFields]()) 230 | return nil 231 | }, 232 | }, 233 | { 234 | name: "anonymous-type.yaml", 235 | setup: func(api *API) error { 236 | api.Post("/test"). 237 | HasRequestModel(ModelOf[struct{ A string }]()). 238 | HasResponseModel(http.StatusOK, ModelOf[struct{ B string }]()) 239 | return nil 240 | }, 241 | }, 242 | { 243 | name: "embedded-structs.yaml", 244 | setup: func(api *API) error { 245 | api.Get("/embedded"). 246 | HasResponseModel(http.StatusOK, ModelOf[EmbeddedStructA]()) 247 | api.Post("/test"). 248 | HasRequestModel(ModelOf[WithEmbeddedStructs]()). 249 | HasResponseModel(http.StatusOK, ModelOf[WithEmbeddedStructs]()) 250 | return nil 251 | }, 252 | }, 253 | { 254 | name: "with-name-struct-tags.yaml", 255 | setup: func(api *API) error { 256 | api.Post("/test"). 257 | HasRequestModel(ModelOf[WithNameStructTags]()). 258 | HasResponseModel(http.StatusOK, ModelOf[WithNameStructTags]()) 259 | return nil 260 | }, 261 | }, 262 | { 263 | name: "known-types.yaml", 264 | setup: func(api *API) error { 265 | api.Route(http.MethodGet, "/test"). 266 | HasResponseModel(http.StatusOK, ModelOf[KnownTypes]()) 267 | return nil 268 | }, 269 | }, 270 | { 271 | name: "all-methods.yaml", 272 | setup: func(api *API) (err error) { 273 | api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[OK]()) 274 | api.Head("/head").HasResponseModel(http.StatusOK, ModelOf[OK]()) 275 | api.Post("/post").HasResponseModel(http.StatusOK, ModelOf[OK]()) 276 | api.Put("/put").HasResponseModel(http.StatusOK, ModelOf[OK]()) 277 | api.Patch("/patch").HasResponseModel(http.StatusOK, ModelOf[OK]()) 278 | api.Delete("/delete").HasResponseModel(http.StatusOK, ModelOf[OK]()) 279 | api.Connect("/connect").HasResponseModel(http.StatusOK, ModelOf[OK]()) 280 | api.Options("/options").HasResponseModel(http.StatusOK, ModelOf[OK]()) 281 | api.Trace("/trace").HasResponseModel(http.StatusOK, ModelOf[OK]()) 282 | return 283 | }, 284 | }, 285 | { 286 | name: "enums.yaml", 287 | setup: func(api *API) (err error) { 288 | // Register the enums and values. 289 | api.RegisterModel(ModelOf[StringEnum](), WithEnumValues(StringEnumA, StringEnumB, StringEnumC)) 290 | api.RegisterModel(ModelOf[IntEnum](), WithEnumValues(IntEnum1, IntEnum2, IntEnum3)) 291 | 292 | api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[WithEnums]()) 293 | return 294 | }, 295 | }, 296 | { 297 | name: "enum-constants.yaml", 298 | setup: func(api *API) (err error) { 299 | // Register the enums and values. 300 | api.RegisterModel(ModelOf[StringEnum](), WithEnumConstants[StringEnum]()) 301 | api.RegisterModel(ModelOf[IntEnum](), WithEnumConstants[IntEnum]()) 302 | 303 | api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[WithEnums]()) 304 | return 305 | }, 306 | }, 307 | { 308 | name: "with-maps.yaml", 309 | setup: func(api *API) (err error) { 310 | api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[WithMaps]()) 311 | return 312 | }, 313 | }, 314 | { 315 | name: "route-params.yaml", 316 | setup: func(api *API) (err error) { 317 | api.Get(`/organisation/{orgId:\d+}/user/{userId}`). 318 | HasPathParameter("orgId", PathParam{ 319 | Description: "Organisation ID", 320 | Regexp: `\d+`, 321 | }). 322 | HasPathParameter("userId", PathParam{ 323 | Description: "User ID", 324 | }). 325 | HasResponseModel(http.StatusOK, ModelOf[User]()) 326 | return 327 | }, 328 | }, 329 | { 330 | name: "route-params.yaml", 331 | setup: func(api *API) (err error) { 332 | api.Get(`/organisation/{orgId:\d+}/user/{userId}`). 333 | HasPathParameter("orgId", PathParam{ 334 | Regexp: `\d+`, 335 | ApplyCustomSchema: func(s *openapi3.Parameter) { 336 | s.Description = "Organisation ID" 337 | }, 338 | }). 339 | HasPathParameter("userId", PathParam{ 340 | Description: "User ID", 341 | }). 342 | HasResponseModel(http.StatusOK, ModelOf[User]()) 343 | return 344 | }, 345 | }, 346 | { 347 | name: "query-params.yaml", 348 | setup: func(api *API) (err error) { 349 | api.Get(`/users?orgId=123&orderBy=field`). 350 | HasQueryParameter("orgId", QueryParam{ 351 | Description: "ID of the organisation", 352 | Required: true, 353 | Type: PrimitiveTypeInteger, 354 | }). 355 | HasQueryParameter("orderBy", QueryParam{ 356 | Description: "The field to order the results by", 357 | Required: false, 358 | Type: PrimitiveTypeString, 359 | Regexp: `field|otherField`, 360 | }). 361 | HasResponseModel(http.StatusOK, ModelOf[User]()) 362 | return 363 | }, 364 | }, 365 | { 366 | name: "query-params.yaml", 367 | setup: func(api *API) (err error) { 368 | api.Get(`/users?orgId=123&orderBy=field`). 369 | HasQueryParameter("orgId", QueryParam{ 370 | Required: true, 371 | Type: PrimitiveTypeInteger, 372 | ApplyCustomSchema: func(s *openapi3.Parameter) { 373 | s.Description = "ID of the organisation" 374 | }, 375 | }). 376 | HasQueryParameter("orderBy", QueryParam{ 377 | Required: false, 378 | Type: PrimitiveTypeString, 379 | Regexp: `field|otherField`, 380 | ApplyCustomSchema: func(s *openapi3.Parameter) { 381 | s.Description = "The field to order the results by" 382 | }, 383 | }). 384 | HasResponseModel(http.StatusOK, ModelOf[User]()) 385 | return 386 | }, 387 | }, 388 | { 389 | name: "multiple-dates-with-comments.yaml", 390 | setup: func(api *API) (err error) { 391 | api.Get("/dates"). 392 | HasResponseModel(http.StatusOK, ModelOf[MultipleDateFieldsWithComments]()) 393 | return 394 | }, 395 | }, 396 | { 397 | name: "custom-models.yaml", 398 | setup: func(api *API) (err error) { 399 | api.Get("/struct-with-customisation"). 400 | HasResponseModel(http.StatusOK, ModelOf[StructWithCustomisation]()) 401 | api.Get("/struct-ptr-with-customisation"). 402 | HasResponseModel(http.StatusOK, ModelOf[*StructWithCustomisation]()) 403 | return 404 | }, 405 | }, 406 | { 407 | name: "global-customisation.yaml", 408 | opts: []APIOpts{ 409 | WithApplyCustomSchemaToType(func(t reflect.Type, s *openapi3.Schema) { 410 | if t != reflect.TypeOf(StructWithTags{}) { 411 | return 412 | } 413 | for fi := 0; fi < t.NumField(); fi++ { 414 | // Get the field name. 415 | var name string 416 | name = t.Field(fi).Tag.Get("json") 417 | if name == "" { 418 | name = t.Field(fi).Name 419 | } 420 | 421 | // Get the custom description from the struct tag. 422 | desc := t.Field(fi).Tag.Get("rest") 423 | if desc == "" { 424 | continue 425 | } 426 | if s.Properties == nil { 427 | s.Properties = make(map[string]*openapi3.SchemaRef) 428 | } 429 | if s.Properties[name] == nil { 430 | s.Properties[name] = &openapi3.SchemaRef{ 431 | Value: &openapi3.Schema{}, 432 | } 433 | } 434 | s.Properties[name].Value.Description = desc 435 | } 436 | }), 437 | }, 438 | setup: func(api *API) error { 439 | api.Get("/"). 440 | HasResponseModel(http.StatusOK, ModelOf[StructWithTags]()) 441 | return nil 442 | }, 443 | }, 444 | } 445 | 446 | for _, test := range tests { 447 | t.Run(test.name, func(t *testing.T) { 448 | var expected, actual []byte 449 | 450 | var wg sync.WaitGroup 451 | wg.Add(2) 452 | errs := make([]error, 2) 453 | 454 | // Validate the test file. 455 | go func() { 456 | defer wg.Done() 457 | // Load test file. 458 | expectedYAML, err := testFiles.ReadFile("tests/" + test.name) 459 | if err != nil { 460 | errs[0] = fmt.Errorf("could not read file %q: %v", test.name, err) 461 | return 462 | } 463 | expectedSpec, err := openapi3.NewLoader().LoadFromData(expectedYAML) 464 | if err != nil { 465 | errs[0] = fmt.Errorf("error in expected YAML: %w", err) 466 | return 467 | } 468 | expected, errs[0] = specToYAML(expectedSpec) 469 | }() 470 | 471 | go func() { 472 | defer wg.Done() 473 | // Create the API. 474 | api := NewAPI(test.name, test.opts...) 475 | api.StripPkgPaths = []string{"github.com/a-h/rest"} 476 | // Configure it. 477 | test.setup(api) 478 | // Create the actual spec. 479 | spec, err := api.Spec() 480 | if err != nil { 481 | t.Errorf("failed to generate spec: %v", err) 482 | } 483 | actual, errs[1] = specToYAML(spec) 484 | }() 485 | 486 | wg.Wait() 487 | var setupFailed bool 488 | for _, err := range errs { 489 | if err != nil { 490 | setupFailed = true 491 | t.Error(err) 492 | } 493 | } 494 | if setupFailed { 495 | t.Fatal("test setup failed") 496 | } 497 | 498 | // Compare the JSON marshalled output to ignore unexported fields and internal state. 499 | if diff := cmp.Diff(expected, actual); diff != "" { 500 | t.Error(diff) 501 | t.Error("\n\n" + string(actual)) 502 | } 503 | }) 504 | } 505 | } 506 | 507 | func specToYAML(spec *openapi3.T) (out []byte, err error) { 508 | // Use JSON, because kin-openapi doesn't customise the YAML output. 509 | // For example, AdditionalProperties only has a MarshalJSON capability. 510 | out, err = json.Marshal(spec) 511 | if err != nil { 512 | err = fmt.Errorf("could not marshal spec to JSON: %w", err) 513 | return 514 | } 515 | var m map[string]interface{} 516 | err = json.Unmarshal(out, &m) 517 | if err != nil { 518 | return 519 | } 520 | return yaml.Marshal(m) 521 | } 522 | -------------------------------------------------------------------------------- /swaggerui/handler.go: -------------------------------------------------------------------------------- 1 | package swaggerui 2 | 3 | import ( 4 | "embed" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/getkin/kin-openapi/openapi3" 10 | ) 11 | 12 | //go:embed swagger-ui/* 13 | var swaggerUI embed.FS 14 | 15 | func New(spec *openapi3.T) (h http.Handler, err error) { 16 | specBytes, err := json.MarshalIndent(spec, "", " ") 17 | if err != nil { 18 | return h, fmt.Errorf("swaggerui: failed to marshal specification: %w", err) 19 | } 20 | 21 | m := http.NewServeMux() 22 | m.Handle("/", http.FileServer(http.FS(swaggerUI))) 23 | m.HandleFunc("/swagger-ui/swagger.json", func(w http.ResponseWriter, r *http.Request) { 24 | w.Header().Add("Content-Type", "application/json") 25 | w.Write(specBytes) 26 | }) 27 | 28 | return m, nil 29 | } 30 | -------------------------------------------------------------------------------- /swaggerui/swagger-ui/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-h/rest/6729b3328f851d4531b2dfc1ab6ffb30d4c8dbe7/swaggerui/swagger-ui/favicon-16x16.png -------------------------------------------------------------------------------- /swaggerui/swagger-ui/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-h/rest/6729b3328f851d4531b2dfc1ab6ffb30d4c8dbe7/swaggerui/swagger-ui/favicon-32x32.png -------------------------------------------------------------------------------- /swaggerui/swagger-ui/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | overflow: -moz-scrollbars-vertical; 4 | overflow-y: scroll; 5 | } 6 | 7 | *, 8 | *:before, 9 | *:after { 10 | box-sizing: inherit; 11 | } 12 | 13 | body { 14 | margin: 0; 15 | background: #fafafa; 16 | } 17 | -------------------------------------------------------------------------------- /swaggerui/swagger-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /swaggerui/swagger-ui/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swagger UI: OAuth2 Redirect 5 | 6 | 7 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /swaggerui/swagger-ui/swagger-initializer.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | // 3 | 4 | // the following lines will be replaced by docker/configurator, when it runs in a docker-container 5 | window.ui = SwaggerUIBundle({ 6 | url: "./swagger.json", 7 | dom_id: '#swagger-ui', 8 | deepLinking: true, 9 | presets: [ 10 | SwaggerUIBundle.presets.apis, 11 | SwaggerUIStandalonePreset 12 | ], 13 | plugins: [ 14 | SwaggerUIBundle.plugins.DownloadUrl 15 | ], 16 | layout: "StandaloneLayout" 17 | }); 18 | 19 | // 20 | }; 21 | -------------------------------------------------------------------------------- /tests/all-methods.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | OK: 5 | type: object 6 | properties: 7 | ok: 8 | type: boolean 9 | required: 10 | - ok 11 | info: 12 | title: all-methods.yaml 13 | version: 0.0.0 14 | paths: 15 | /connect: 16 | connect: 17 | responses: 18 | "200": 19 | description: "" 20 | content: 21 | application/json: 22 | schema: 23 | $ref: '#/components/schemas/OK' 24 | default: 25 | description: "" 26 | /delete: 27 | delete: 28 | responses: 29 | "200": 30 | description: "" 31 | content: 32 | application/json: 33 | schema: 34 | $ref: '#/components/schemas/OK' 35 | default: 36 | description: "" 37 | /get: 38 | get: 39 | responses: 40 | "200": 41 | description: "" 42 | content: 43 | application/json: 44 | schema: 45 | $ref: '#/components/schemas/OK' 46 | default: 47 | description: "" 48 | /head: 49 | head: 50 | responses: 51 | "200": 52 | description: "" 53 | content: 54 | application/json: 55 | schema: 56 | $ref: '#/components/schemas/OK' 57 | default: 58 | description: "" 59 | /options: 60 | options: 61 | responses: 62 | "200": 63 | description: "" 64 | content: 65 | application/json: 66 | schema: 67 | $ref: '#/components/schemas/OK' 68 | default: 69 | description: "" 70 | /patch: 71 | patch: 72 | responses: 73 | "200": 74 | description: "" 75 | content: 76 | application/json: 77 | schema: 78 | $ref: '#/components/schemas/OK' 79 | default: 80 | description: "" 81 | /post: 82 | post: 83 | responses: 84 | "200": 85 | description: "" 86 | content: 87 | application/json: 88 | schema: 89 | $ref: '#/components/schemas/OK' 90 | default: 91 | description: "" 92 | /put: 93 | put: 94 | responses: 95 | "200": 96 | description: "" 97 | content: 98 | application/json: 99 | schema: 100 | $ref: '#/components/schemas/OK' 101 | default: 102 | description: "" 103 | /trace: 104 | trace: 105 | responses: 106 | "200": 107 | description: "" 108 | content: 109 | application/json: 110 | schema: 111 | $ref: '#/components/schemas/OK' 112 | default: 113 | description: "" 114 | 115 | -------------------------------------------------------------------------------- /tests/anonymous-type.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | AnonymousType0: 5 | type: object 6 | properties: 7 | A: 8 | type: string 9 | required: 10 | - A 11 | AnonymousType1: 12 | type: object 13 | properties: 14 | B: 15 | type: string 16 | required: 17 | - B 18 | info: 19 | title: anonymous-type.yaml 20 | version: 0.0.0 21 | paths: 22 | /test: 23 | post: 24 | requestBody: 25 | content: 26 | application/json: 27 | schema: 28 | $ref: '#/components/schemas/AnonymousType0' 29 | responses: 30 | "200": 31 | description: "" 32 | content: 33 | application/json: 34 | schema: 35 | $ref: '#/components/schemas/AnonymousType1' 36 | default: 37 | description: "" 38 | 39 | -------------------------------------------------------------------------------- /tests/basic-data-types-pointers.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | AllBasicDataTypesPointers: 5 | type: object 6 | properties: 7 | Bool: 8 | type: boolean 9 | nullable: true 10 | Byte: 11 | type: integer 12 | nullable: true 13 | Float32: 14 | type: number 15 | nullable: true 16 | Float64: 17 | type: number 18 | nullable: true 19 | Int: 20 | type: integer 21 | nullable: true 22 | Int8: 23 | type: integer 24 | nullable: true 25 | Int16: 26 | type: integer 27 | nullable: true 28 | Int32: 29 | type: integer 30 | nullable: true 31 | Int64: 32 | type: integer 33 | nullable: true 34 | Rune: 35 | type: integer 36 | nullable: true 37 | String: 38 | type: string 39 | nullable: true 40 | Uint: 41 | type: integer 42 | nullable: true 43 | Uint8: 44 | type: integer 45 | nullable: true 46 | Uint16: 47 | type: integer 48 | nullable: true 49 | Uint32: 50 | type: integer 51 | nullable: true 52 | Uint64: 53 | type: integer 54 | nullable: true 55 | Uintptr: 56 | type: integer 57 | nullable: true 58 | info: 59 | title: basic-data-types-pointers.yaml 60 | version: 0.0.0 61 | paths: 62 | /test: 63 | post: 64 | requestBody: 65 | content: 66 | application/json: 67 | schema: 68 | $ref: '#/components/schemas/AllBasicDataTypesPointers' 69 | responses: 70 | "200": 71 | description: "" 72 | content: 73 | application/json: 74 | schema: 75 | $ref: '#/components/schemas/AllBasicDataTypesPointers' 76 | default: 77 | description: "" 78 | -------------------------------------------------------------------------------- /tests/basic-data-types.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | AllBasicDataTypes: 5 | properties: 6 | Bool: 7 | type: boolean 8 | Byte: 9 | type: integer 10 | Float32: 11 | type: number 12 | Float64: 13 | type: number 14 | Int: 15 | type: integer 16 | Int8: 17 | type: integer 18 | Int16: 19 | type: integer 20 | Int32: 21 | type: integer 22 | Int64: 23 | type: integer 24 | Rune: 25 | type: integer 26 | String: 27 | type: string 28 | Uint: 29 | type: integer 30 | Uint8: 31 | type: integer 32 | Uint16: 33 | type: integer 34 | Uint32: 35 | type: integer 36 | Uint64: 37 | type: integer 38 | Uintptr: 39 | type: integer 40 | required: 41 | - Int 42 | - Int8 43 | - Int16 44 | - Int32 45 | - Int64 46 | - Uint 47 | - Uint8 48 | - Uint16 49 | - Uint32 50 | - Uint64 51 | - Uintptr 52 | - Float32 53 | - Float64 54 | - Byte 55 | - Rune 56 | - String 57 | - Bool 58 | type: object 59 | info: 60 | title: basic-data-types.yaml 61 | version: 0.0.0 62 | paths: 63 | /test: 64 | post: 65 | operationId: "postAllBasicDataTypes" 66 | description: "Post all basic data types description" 67 | requestBody: 68 | content: 69 | application/json: 70 | schema: 71 | $ref: '#/components/schemas/AllBasicDataTypes' 72 | responses: 73 | "200": 74 | description: "" 75 | content: 76 | application/json: 77 | schema: 78 | $ref: '#/components/schemas/AllBasicDataTypes' 79 | default: 80 | description: "" 81 | tags: 82 | - BasicData 83 | -------------------------------------------------------------------------------- /tests/custom-models.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | StructWithCustomisation: 4 | properties: 5 | a: 6 | description: A string 7 | example: "test" 8 | type: string 9 | b: 10 | description: A custom field 11 | type: string 12 | format: custom 13 | example: "model_field_customisation" 14 | c: 15 | nullable: true 16 | type: string 17 | format: custom 18 | example: "model_field_customisation" 19 | required: 20 | - a 21 | - b 22 | type: object 23 | info: 24 | title: custom-models.yaml 25 | version: 0.0.0 26 | openapi: 3.0.0 27 | paths: 28 | /struct-ptr-with-customisation: 29 | get: 30 | responses: 31 | "200": 32 | content: 33 | application/json: 34 | schema: 35 | $ref: '#/components/schemas/StructWithCustomisation' 36 | description: "" 37 | default: 38 | description: "" 39 | /struct-with-customisation: 40 | get: 41 | responses: 42 | "200": 43 | content: 44 | application/json: 45 | schema: 46 | $ref: '#/components/schemas/StructWithCustomisation' 47 | description: "" 48 | default: 49 | description: "" 50 | -------------------------------------------------------------------------------- /tests/embedded-structs.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | EmbeddedStructA: 5 | type: object 6 | properties: 7 | A: 8 | type: string 9 | required: 10 | - A 11 | WithEmbeddedStructs: 12 | type: object 13 | properties: 14 | A: 15 | type: string 16 | B: 17 | type: string 18 | OptionalB: 19 | type: string 20 | PointerB: 21 | nullable: true 22 | type: string 23 | OptionalPointerB: 24 | nullable: true 25 | type: string 26 | C: 27 | type: string 28 | required: 29 | - A 30 | - B 31 | - C 32 | info: 33 | title: embedded-structs.yaml 34 | version: 0.0.0 35 | paths: 36 | /embedded: 37 | get: 38 | responses: 39 | "200": 40 | description: "" 41 | content: 42 | application/json: 43 | schema: 44 | $ref: '#/components/schemas/EmbeddedStructA' 45 | default: 46 | description: "" 47 | /test: 48 | post: 49 | requestBody: 50 | content: 51 | application/json: 52 | schema: 53 | $ref: '#/components/schemas/WithEmbeddedStructs' 54 | responses: 55 | "200": 56 | description: "" 57 | content: 58 | application/json: 59 | schema: 60 | $ref: '#/components/schemas/WithEmbeddedStructs' 61 | default: 62 | description: "" 63 | 64 | -------------------------------------------------------------------------------- /tests/enum-constants.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | IntEnum: 5 | type: integer 6 | enum: 7 | - 1 8 | - 2 9 | - 3 10 | StringEnum: 11 | type: string 12 | enum: 13 | - A 14 | - B 15 | - B 16 | WithEnums: 17 | type: object 18 | properties: 19 | i: 20 | $ref: '#/components/schemas/IntEnum' 21 | s: 22 | $ref: '#/components/schemas/StringEnum' 23 | ss: 24 | type: array 25 | nullable: true 26 | items: 27 | $ref: '#/components/schemas/StringEnum' 28 | v: 29 | type: string 30 | required: 31 | - s 32 | - ss 33 | - i 34 | - v 35 | info: 36 | title: enum-constants.yaml 37 | version: 0.0.0 38 | paths: 39 | /get: 40 | get: 41 | responses: 42 | "200": 43 | description: "" 44 | content: 45 | application/json: 46 | schema: 47 | $ref: '#/components/schemas/WithEnums' 48 | default: 49 | description: "" 50 | -------------------------------------------------------------------------------- /tests/enums.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | IntEnum: 5 | type: integer 6 | enum: 7 | - 1 8 | - 2 9 | - 3 10 | StringEnum: 11 | type: string 12 | enum: 13 | - A 14 | - B 15 | - B 16 | WithEnums: 17 | type: object 18 | properties: 19 | i: 20 | $ref: '#/components/schemas/IntEnum' 21 | s: 22 | $ref: '#/components/schemas/StringEnum' 23 | ss: 24 | type: array 25 | nullable: true 26 | items: 27 | $ref: '#/components/schemas/StringEnum' 28 | v: 29 | type: string 30 | required: 31 | - s 32 | - ss 33 | - i 34 | - v 35 | info: 36 | title: enums.yaml 37 | version: 0.0.0 38 | paths: 39 | /get: 40 | get: 41 | responses: 42 | "200": 43 | description: "" 44 | content: 45 | application/json: 46 | schema: 47 | $ref: '#/components/schemas/WithEnums' 48 | default: 49 | description: "" 50 | -------------------------------------------------------------------------------- /tests/global-customisation.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | StructWithTags: 5 | properties: 6 | a: 7 | description: A is a string. 8 | type: string 9 | required: 10 | - a 11 | type: object 12 | info: 13 | title: global-customisation.yaml 14 | version: 0.0.0 15 | paths: 16 | /: 17 | get: 18 | responses: 19 | "200": 20 | content: 21 | application/json: 22 | schema: 23 | $ref: '#/components/schemas/StructWithTags' 24 | description: "" 25 | default: 26 | description: "" 27 | -------------------------------------------------------------------------------- /tests/known-types.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | KnownTypes: 5 | type: object 6 | properties: 7 | time: 8 | type: string 9 | format: date-time 10 | timePtr: 11 | type: string 12 | format: date-time 13 | nullable: true 14 | required: 15 | - time 16 | info: 17 | title: known-types.yaml 18 | version: 0.0.0 19 | paths: 20 | /test: 21 | get: 22 | responses: 23 | "200": 24 | description: "" 25 | content: 26 | application/json: 27 | schema: 28 | $ref: '#/components/schemas/KnownTypes' 29 | default: 30 | description: "" 31 | 32 | -------------------------------------------------------------------------------- /tests/multiple-dates-with-comments.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | MultipleDateFieldsWithComments: 5 | properties: 6 | dateField: 7 | description: DateField is a field containing a date 8 | format: date-time 9 | type: string 10 | dateFieldA: 11 | description: DateFieldA is another field containing a date 12 | format: date-time 13 | type: string 14 | required: 15 | - dateField 16 | - dateFieldA 17 | type: object 18 | info: 19 | title: multiple-dates-with-comments.yaml 20 | version: 0.0.0 21 | paths: 22 | /dates: 23 | get: 24 | responses: 25 | "200": 26 | content: 27 | application/json: 28 | schema: 29 | $ref: '#/components/schemas/MultipleDateFieldsWithComments' 30 | description: "" 31 | default: 32 | description: "" 33 | -------------------------------------------------------------------------------- /tests/omit-empty-fields.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | OmitEmptyFields: 5 | properties: 6 | A: 7 | type: string 8 | B: 9 | type: string 10 | C: 11 | nullable: true 12 | type: string 13 | D: 14 | nullable: true 15 | type: string 16 | required: 17 | - A 18 | type: object 19 | info: 20 | title: omit-empty-fields.yaml 21 | version: 0.0.0 22 | paths: 23 | /test: 24 | post: 25 | requestBody: 26 | content: 27 | application/json: 28 | schema: 29 | $ref: '#/components/schemas/OmitEmptyFields' 30 | responses: 31 | "200": 32 | content: 33 | application/json: 34 | schema: 35 | $ref: '#/components/schemas/OmitEmptyFields' 36 | description: "" 37 | default: 38 | description: "" 39 | 40 | -------------------------------------------------------------------------------- /tests/query-params.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | User: 5 | properties: 6 | id: 7 | type: integer 8 | name: 9 | type: string 10 | required: 11 | - id 12 | - name 13 | type: object 14 | info: 15 | title: query-params.yaml 16 | version: 0.0.0 17 | paths: 18 | /users?orgId=123&orderBy=field: 19 | get: 20 | parameters: 21 | - in: query 22 | description: The field to order the results by 23 | name: orderBy 24 | required: false 25 | schema: 26 | type: string 27 | pattern: field|otherField 28 | - in: query 29 | description: ID of the organisation 30 | name: orgId 31 | required: true 32 | schema: 33 | type: integer 34 | responses: 35 | "200": 36 | description: "" 37 | content: 38 | application/json: 39 | schema: 40 | $ref: '#/components/schemas/User' 41 | default: 42 | description: "" 43 | -------------------------------------------------------------------------------- /tests/route-params.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | User: 5 | properties: 6 | id: 7 | type: integer 8 | name: 9 | type: string 10 | required: 11 | - id 12 | - name 13 | type: object 14 | info: 15 | title: route-params.yaml 16 | version: 0.0.0 17 | paths: 18 | /organisation/{orgId:\d+}/user/{userId}: 19 | get: 20 | parameters: 21 | - in: path 22 | description: Organisation ID 23 | name: orgId 24 | required: true 25 | schema: 26 | type: string 27 | pattern: \d+ 28 | - in: path 29 | description: User ID 30 | name: userId 31 | required: true 32 | schema: 33 | type: string 34 | responses: 35 | "200": 36 | description: "" 37 | content: 38 | application/json: 39 | schema: 40 | $ref: '#/components/schemas/User' 41 | default: 42 | description: "" 43 | -------------------------------------------------------------------------------- /tests/test000.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: {} 3 | info: 4 | title: test000.yaml 5 | version: 0.0.0 6 | paths: {} 7 | 8 | -------------------------------------------------------------------------------- /tests/test001.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | TestRequestType: 5 | properties: 6 | IntField: 7 | type: integer 8 | required: 9 | - IntField 10 | type: object 11 | TestResponseType: 12 | description: TestResponseType description. 13 | properties: 14 | IntField: 15 | description: IntField description. 16 | type: integer 17 | required: 18 | - IntField 19 | type: object 20 | info: 21 | title: test001.yaml 22 | version: 0.0.0 23 | paths: 24 | /test: 25 | post: 26 | description: "Test request type description" 27 | requestBody: 28 | content: 29 | application/json: 30 | schema: 31 | $ref: '#/components/schemas/TestRequestType' 32 | responses: 33 | "200": 34 | content: 35 | application/json: 36 | schema: 37 | $ref: '#/components/schemas/TestResponseType' 38 | description: "" 39 | default: 40 | description: "" 41 | tags: 42 | - TestRequest 43 | -------------------------------------------------------------------------------- /tests/with-maps.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | WithMaps: 5 | properties: 6 | amounts: 7 | additionalProperties: 8 | type: integer 9 | nullable: true 10 | type: object 11 | required: 12 | - amounts 13 | type: object 14 | info: 15 | title: with-maps.yaml 16 | version: 0.0.0 17 | paths: 18 | /get: 19 | get: 20 | responses: 21 | "200": 22 | description: "" 23 | content: 24 | application/json: 25 | schema: 26 | $ref: '#/components/schemas/WithMaps' 27 | default: 28 | description: "" 29 | 30 | -------------------------------------------------------------------------------- /tests/with-name-struct-tags.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | components: 3 | schemas: 4 | WithNameStructTags: 5 | type: object 6 | properties: 7 | firstName: 8 | description: FirstName of something. 9 | type: string 10 | LastName: 11 | description: LastName of something. 12 | type: string 13 | FullName: 14 | deprecated: true 15 | description: |- 16 | FullName of something. 17 | Deprecated: Use FirstName and LastName 18 | type: string 19 | MiddleName: 20 | description: |- 21 | MiddleName of something. Deprecated: This deprecation flag is not valid so this field should 22 | not be marked as deprecated. 23 | type: string 24 | required: 25 | - firstName 26 | - LastName 27 | - FullName 28 | - MiddleName 29 | info: 30 | title: with-name-struct-tags.yaml 31 | version: 0.0.0 32 | paths: 33 | /test: 34 | post: 35 | requestBody: 36 | content: 37 | application/json: 38 | schema: 39 | $ref: '#/components/schemas/WithNameStructTags' 40 | responses: 41 | "200": 42 | description: "" 43 | content: 44 | application/json: 45 | schema: 46 | $ref: '#/components/schemas/WithNameStructTags' 47 | default: 48 | description: "" 49 | 50 | --------------------------------------------------------------------------------