github.com/thetreep/go-swagger@v0.0.0-20240223100711-35af64f14f01/docs/tutorial/dynamic.md (about) 1 # Dynamic API definition 2 3 The toolkit supports building a swagger specification entirely with go code. It does allow you to serve a spec up quickly. This is one of the building blocks required to serve up stub APIs and to generate a test server with predictable responses, however this is not as bad as it sounds... 4 5 <!--more--> 6 7 This tutorial uses the todo list application to serve a swagger based API defined entirely in go code. 8 Because we know what we want the spec to look like, first we'll just build the entire spec with the internal dsl. 9 10 ## Loading the specification 11 12 ```go 13 package main 14 15 import ( 16 "log" 17 "os" 18 19 "github.com/go-openapi/loads" 20 "github.com/go-openapi/loads/fmts" 21 ) 22 23 func init() { 24 loads.AddLoader(fmts.YAMLMatcher, fmts.YAMLDoc) 25 } 26 27 func main() { 28 if len(os.Args) == 1 { 29 log.Fatalln("this command requires the swagger spec as argument") 30 } 31 log.Printf("loading %q as contract for the server", os.Args[1]) 32 33 specDoc, err := loads.Spec(os.Args[1]) 34 if err != nil { 35 log.Fatalln(err) 36 } 37 38 log.Println("Would be serving:", specDoc.Spec().Info.Title) 39 } 40 ``` 41 42 [see source of this code](https://github.com/thetreep/go-swagger/blob/master/examples/tutorials/todo-list/dynamic-1/main.go) 43 44 Running this would confirm that we can in fact read a swagger spec from disk. 45 The init method enables loading of yaml based specifications. The yaml package for golang used to be licensed as GPL so we made depending on it optional. 46 47 ``` 48 git:(master) ✗ !? » go run main.go ./swagger.yml 49 2016/10/08 20:50:42 loading "./swagger.yml" as contract for the server 50 2016/10/08 20:50:42 Would be serving: A To Do list application 51 ``` 52 53 ## Setup 54 55 Before we can implement our API we'll look at setting up the server for our openapi spec. 56 Go-swagger wants you to configure your API with an api descriptor so that it knows how to handle requests. 57 58 ### Validation of requirements 59 60 It's probably a good idea to fail starting the server when it can't fulfill all the requests defined in the swagger spec. 61 So let's start by enabling that validation: 62 63 ```go 64 func main() { 65 if len(os.Args) == 1 { 66 log.Fatalln("this command requires the swagger spec as argument") 67 } 68 log.Printf("loading %q as contract for the server", os.Args[1]) 69 70 specDoc, err := loads.Spec(os.Args[1]) 71 if err != nil { 72 log.Fatalln(err) 73 } 74 75 api := untyped.NewAPI(specDoc) 76 77 // validate the API descriptor, to ensure we don't have any unhandled operations 78 if err := api.Validate(); err != nil { 79 log.Fatalln(err) 80 } 81 log.Println("serving:", specDoc.Spec().Info.Title) 82 83 } 84 ``` 85 86 [see source of this code](https://github.com/thetreep/go-swagger/blob/master/examples/tutorials/todo-list/dynamic-setup-invalid/main.go) 87 88 This code shows how to create an api descriptor and then invoking its verification. 89 Because our specification contains operations and consumes/produces definitions this program should not run. 90 When we try to run it, it should exit with a non-zero status. 91 92 ``` 93 git:(master) ✗ -? » go run main.go ./swagger.yml 94 2016/10/08 21:32:14 loading "./swagger.yml" as contract for the server 95 2016/10/08 21:32:14 missing [application/io.goswagger.examples.todo-list.v1+json] consumes registrations 96 missing from spec file [application/json] consumes 97 exit status 1 98 ``` 99 100 ### Satisfying validation with stubs 101 102 For us to be able to start our server we will register the right serializers and we'll stub out the operation handlers with a not implemented handler. 103 104 ```go 105 func main() { 106 if len(os.Args) == 1 { 107 log.Fatalln("this command requires the swagger spec as argument") 108 } 109 log.Printf("loading %q as contract for the server", os.Args[1]) 110 111 specDoc, err := loads.Spec(os.Args[1]) 112 if err != nil { 113 log.Fatalln(err) 114 } 115 116 // our spec doesn't have application/json in the consumes or produces 117 // so we need to clear those settings out 118 api := untyped.NewAPI(specDoc).WithoutJSONDefaults() 119 120 // register serializers 121 mediaType := "application/io.goswagger.examples.todo-list.v1+json" 122 api.DefaultConsumes = mediaType 123 api.DefaultProduces = mediaType 124 api.RegisterConsumer(mediaType, runtime.JSONConsumer()) 125 api.RegisterProducer(mediaType, runtime.JSONProducer()) 126 127 api.RegisterOperation("GET", "/", notImplemented) 128 api.RegisterOperation("POST", "/", notImplemented) 129 api.RegisterOperation("PUT", "/{id}", notImplemented) 130 api.RegisterOperation("DELETE", "/{id}", notImplemented) 131 132 // validate the API descriptor, to ensure we don't have any unhandled operations 133 if err := api.Validate(); err != nil { 134 log.Fatalln(err) 135 } 136 137 // construct the application context for this server 138 // use the loaded spec document and the api descriptor with the default router 139 app := middleware.NewContext(specDoc, api, nil) 140 141 log.Println("serving", specDoc.Spec().Info.Title, "at http://localhost:8000") 142 // serve the api 143 if err := http.ListenAndServe(":8000", app.APIHandler(nil)); err != nil { 144 log.Fatalln(err) 145 } 146 } 147 148 var notImplemented = runtime.OperationHandlerFunc(func(params interface{}) (interface{}, error) { 149 return middleware.NotImplemented("not implemented"), nil 150 }) 151 ``` 152 153 The untyped API for go-swagger assumes by default you want to serve `application/json` and initializes the descriptor with default values to that effect. 154 In our spec however we don't serve 'application/json' which means we have to use `WithoutJSONDefaults` when we initialize our api. 155 156 The media type we do know is: `application/io.goswagger.examples.todo-list.v1+json`, this is also a json format. 157 We set it as defaults and register the appropriate consumer and producer functions. 158 159 Our specification has 4 methods: findTodos, addOne, updateOne and destroyOne. Because we have no implementation yet, we register a notImplemented handler for all of them. 160 161 Our api descriptor validation is now satisfied, so we use the simplest way to start a http server in go on port 8000. 162 163 Server terminal: 164 165 ``` 166 git:(master) ✗ -!? » go run main.go ./swagger.yml 167 2016/10/08 23:35:18 loading "./swagger.yml" as contract for the server 168 2016/10/08 23:35:18 serving A To Do list application at http://localhost:8000 169 ``` 170 171 Client terminal: 172 173 ``` 174 git:(master) ✗ -!? » curl -i localhost:8000 175 ``` 176 177 ```http 178 HTTP/1.1 501 Not Implemented 179 Content-Type: application/io.goswagger.examples.todo-list.v1+json 180 Date: Sun, 09 Oct 2016 06:36:11 GMT 181 Content-Length: 18 182 183 "not implemented" 184 ``` 185 186 > There is a lot more to be done to make this server a production ready server, but for the 187 > purpose of this tutorial, this is enough. 188 189 ## Completely untyped 190 191 At this point we're ready to actually implement some functionality for our Todo list. We'll create methods to add, update and delete an item. 192 We'll also render a list of known items. Because http APIs can get concurrent access we need to take care of this as well. 193 194 The first thing we'll do is build our "backend", a very simple implementation based on a slice and maps. 195 196 ```go 197 var items = []map[string]interface{}{ 198 map[string]interface{}{"id": int64(1), "description": "feed dog", "completed": true}, 199 map[string]interface{}{"id": int64(2), "description": "feed cat"}, 200 } 201 202 var itemsLock = &sync.Mutex{} 203 var lastItemID int64 = 2 204 205 func newItemID() int64 { 206 return atomic.AddInt64(&lastItemID, 1) 207 } 208 209 func addItem(item map[string]interface{}) { 210 itemsLock.Lock() 211 defer itemsLock.Unlock() 212 item["id"] = newItemID() 213 items = append(items, item) 214 } 215 216 func updateItem(id int64, body map[string]interface{}) (map[string]interface{}, error) { 217 itemsLock.Lock() 218 defer itemsLock.Unlock() 219 220 item, err := itemByID(id) 221 if err != nil { 222 return nil, err 223 } 224 delete(body, "id") 225 for k, v := range body { 226 item[k] = v 227 } 228 return item, nil 229 } 230 231 func removeItem(id int64) { 232 itemsLock.Lock() 233 defer itemsLock.Unlock() 234 235 var newItems []map[string]interface{} 236 for _, item := range items { 237 if item["id"].(int64) != id { 238 newItems = append(newItems, item) 239 } 240 } 241 items = newItems 242 } 243 244 func itemByID(id int64) (map[string]interface{}, error) { 245 for _, item := range items { 246 if item["id"].(int64) == id { 247 return item, nil 248 } 249 } 250 return nil, errors.NotFound("not found: item %d", id) 251 } 252 ``` 253 254 [see source of this code](https://github.com/thetreep/go-swagger/blob/master/examples/tutorials/todo-list/dynamic-untyped/main.go) 255 256 The backend code builds a todo-list-item store that's save for concurrent access buy guarding every operation with a lock. This is all in memory so as soon as you quit the process all your changes will be reset. 257 258 Because we now have an actual implementation that we can use for testings, lets hook that up in our API: 259 260 ```go 261 func main() { 262 if len(os.Args) == 1 { 263 log.Fatalln("this command requires the swagger spec as argument") 264 } 265 log.Printf("loading %q as contract for the server", os.Args[1]) 266 267 specDoc, err := loads.Spec(os.Args[1]) 268 if err != nil { 269 log.Fatalln(err) 270 } 271 272 // our spec doesn't have application/json in the consumes or produces 273 // so we need to clear those settings out 274 api := untyped.NewAPI(specDoc).WithoutJSONDefaults() 275 276 // register serializers 277 mediaType := "application/io.goswagger.examples.todo-list.v1+json" 278 api.DefaultConsumes = mediaType 279 api.DefaultProduces = mediaType 280 api.RegisterConsumer(mediaType, runtime.JSONConsumer()) 281 api.RegisterProducer(mediaType, runtime.JSONProducer()) 282 283 // register the operation handlers 284 api.RegisterOperation("GET", "/", findTodos) 285 api.RegisterOperation("POST", "/", addOne) 286 api.RegisterOperation("PUT", "/{id}", updateOne) 287 api.RegisterOperation("DELETE", "/{id}", destroyOne) 288 289 // validate the API descriptor, to ensure we don't have any unhandled operations 290 if err := api.Validate(); err != nil { 291 log.Fatalln(err) 292 } 293 294 // construct the application context for this server 295 // use the loaded spec document and the api descriptor with the default router 296 app := middleware.NewContext(specDoc, api, nil) 297 298 log.Println("serving", specDoc.Spec().Info.Title, "at http://localhost:8000") 299 300 // serve the api with spec and UI 301 if err := http.ListenAndServe(":8000", app.APIHandler(nil)); err != nil { 302 log.Fatalln(err) 303 } 304 } 305 306 var findTodos = runtime.OperationHandlerFunc(func(params interface{}) (interface{}, error) { 307 log.Println("received 'findTodos'") 308 log.Printf("%#v\n", params) 309 310 return items, nil 311 }) 312 313 var addOne = runtime.OperationHandlerFunc(func(params interface{}) (interface{}, error) { 314 log.Println("received 'addOne'") 315 log.Printf("%#v\n", params) 316 317 body := params.(map[string]interface{})["body"].(map[string]interface{}) 318 addItem(body) 319 return body, nil 320 }) 321 322 var updateOne = runtime.OperationHandlerFunc(func(params interface{}) (interface{}, error) { 323 log.Println("received 'updateOne'") 324 log.Printf("%#v\n", params) 325 326 data := params.(map[string]interface{}) 327 id := data["id"].(int64) 328 body := data["body"].(map[string]interface{}) 329 return updateItem(id, body) 330 }) 331 332 var destroyOne = runtime.OperationHandlerFunc(func(params interface{}) (interface{}, error) { 333 log.Println("received 'destroyOne'") 334 log.Printf("%#v\n", params) 335 336 removeItem(params.(map[string]interface{})["id"].(int64)) 337 return nil, nil 338 }) 339 ``` 340 341 [see source of this code](https://github.com/thetreep/go-swagger/blob/master/examples/tutorials/todo-list/dynamic-untyped/main.go) 342 343 With this set up we should be able to start a server, send it some requests and get some meaningful answers. 344 345 #### List all 346 347 ``` 348 git:(master) ✗ !? » curl -i localhost:8000 349 ``` 350 351 ```http 352 HTTP/1.1 200 OK 353 Content-Type: application/io.goswagger.examples.todo-list.v1+json 354 Date: Sun, 09 Oct 2016 15:50:39 GMT 355 Content-Length: 87 356 357 [{"completed":true,"description":"feed dog","id":1},{"description":"feed cat","id":2}] 358 ``` 359 360 #### Create new 361 362 The default curl POST request should fail because we only allow: application/io.goswagger.examples.todo-list.v1+json 363 364 ``` 365 curl -i localhost:8000 -d '{"description":"item for the list"}' 366 ``` 367 368 ```http 369 HTTP/1.1 415 Unsupported Media Type 370 Content-Type: application/io.goswagger.examples.todo-list.v1+json 371 Date: Sun, 09 Oct 2016 15:55:43 GMT 372 Content-Length: 157 373 374 {"code":415,"message":"unsupported media type \"application/x-www-form-urlencoded\", only [application/io.goswagger.examples.todo-list.v1+json] are allowed"} 375 ``` 376 377 When the content type header is sent, we have a better result: 378 379 ``` 380 curl -i -H 'Content-Type: application/io.goswagger.examples.todo-list.v1+json' localhost:8000 -d '{"description":"a new item"}' 381 ``` 382 383 ```http 384 HTTP/1.1 201 Created 385 Content-Type: application/io.goswagger.examples.todo-list.v1+json 386 Date: Sun, 09 Oct 2016 15:56:28 GMT 387 Content-Length: 36 388 389 {"description":"a new item","id":3} 390 ``` 391 392 #### List again 393 394 ``` 395 git:(master) ✗ !? » curl -i localhost:8000 396 ``` 397 398 ```http 399 HTTP/1.1 200 OK 400 Content-Type: application/io.goswagger.examples.todo-list.v1+json 401 Date: Sun, 09 Oct 2016 15:58:06 GMT 402 Content-Length: 123 403 404 [{"completed":true,"description":"feed dog","id":1},{"description":"feed cat","id":2},{"description":"a new item","id":3}] 405 ``` 406 407 #### Update an item 408 409 ``` 410 curl -i -XPUT -H 'Content-Type: application/io.goswagger.examples.todo-list.v1+json' localhost:8000/3 -d '{"description":"an updated item"}' 411 ``` 412 413 ```http 414 HTTP/1.1 200 OK 415 Content-Type: application/io.goswagger.examples.todo-list.v1+json 416 Date: Sun, 09 Oct 2016 15:58:42 GMT 417 Content-Length: 41 418 419 {"description":"an updated item","id":3} 420 ``` 421 422 #### List to verify 423 424 ``` 425 git:(master) ✗ !? » curl -i localhost:8000 426 ``` 427 428 ```http 429 HTTP/1.1 200 OK 430 Content-Type: application/io.goswagger.examples.todo-list.v1+json 431 Date: Sun, 09 Oct 2016 15:58:42 GMT 432 Content-Length: 41 433 434 {"description":"an updated item","id":3} 435 ``` 436 437 #### Delete an item 438 439 ``` 440 curl -i -XDELETE localhost:8000/3 441 ``` 442 443 ```http 444 HTTP/1.1 204 No Content 445 Content-Type: application/io.goswagger.examples.todo-list.v1+json 446 Date: Sun, 09 Oct 2016 16:00:59 GMT 447 ``` 448 449 #### List to show start state again 450 451 ``` 452 curl -i localhost:8000 453 ``` 454 455 ```http 456 HTTP/1.1 200 OK 457 Content-Type: application/io.goswagger.examples.todo-list.v1+json 458 Date: Sun, 09 Oct 2016 16:02:19 GMT 459 Content-Length: 87 460 461 [{"completed":true,"description":"feed dog","id":1},{"description":"feed cat","id":2}] 462 ``` 463 464 At the end of the curl requests the server shows these outputs: 465 466 ``` 467 git:(master) ✗ !? » go run main.go ./swagger.yml 468 2016/10/09 08:50:34 loading "./swagger.yml" as contract for the server 469 2016/10/09 08:50:34 serving A To Do list application at http://localhost:8000 470 2016/10/09 08:50:39 received 'findTodos' 471 2016/10/09 08:50:39 map[string]interface {}{"since":0, "limit":20} 472 2016/10/09 08:56:28 received 'addOne' 473 2016/10/09 08:56:28 map[string]interface {}{"body":map[string]interface {}{"description":"a new item"}} 474 2016/10/09 08:58:06 received 'findTodos' 475 2016/10/09 08:58:06 map[string]interface {}{"limit":20, "since":0} 476 2016/10/09 08:58:42 received 'updateOne' 477 2016/10/09 08:58:42 map[string]interface {}{"id":3, "body":map[string]interface {}{"description":"an updated item"}} 478 2016/10/09 09:00:07 received 'findTodos' 479 2016/10/09 09:00:07 map[string]interface {}{"since":0, "limit":20} 480 2016/10/09 09:00:59 received 'destroyOne' 481 2016/10/09 09:00:59 map[string]interface {}{"id":3} 482 2016/10/09 09:02:19 received 'findTodos' 483 2016/10/09 09:02:19 map[string]interface {}{"since":0, "limit":20} 484 ```