github.com/rzurga/go-swagger@v0.28.1-0.20211109195225-5d1f453ffa3a/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/go-swagger/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/go-swagger/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/go-swagger/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/go-swagger/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  ```