The first two parts of my swagger tutorial [Part 1, Part 2] were dedicated to the straightforward art of getting swagger up and running. While I hope they're helpful, the whole point of those was to get you to the point where you had the Timezone project, so I could show you how to add Command Line Arguments to a Swagger microservice.
One thing that I emphasized in Go Swagger Part 2 was that configure_timeofday.go
is the only file you should be touching, it's the interface between the server and your business logic. Every example of adding new flags to the command line, even the one provided by the GoSwagger authors, starts by modifying the file cmd/<project>-server/main.go
, one of those files clearly marked // DO NOT EDIT
.
We're not going to edit files marked // DO NOT EDIT
.
To understand the issue, though, we have to understand the tool swagger uses for handling command line arguments, go-flags
.
Go-Flags
go-flags
is the tool Swagger uses by default for handling command line arguments. It's a clever tool that uses Go's tags and reflection features to encode the details of the CLI directly into a structure that will hold the options passed in on the command line.
The implementation
We're going to add a single feature: the default timezone. In Part 2, we hard-coded the default timezone into the handler, but what if we want to change the default timezone more readily than recompiling the binary every time? The Go, Docker, and Kubernetes crowd argue that that's acceptable, but I still want more flexibility.
To start, we're going to open a new file it the folder with our handlers and add a new file, timezone.go
. We're going to put a single tagged structure with a single field to hold our timezone CLI argument, and we're going to use go-flags
protocol to describe our new command line flag.
Here's the whole file:
package timeofday
type Timezone struct {
Timezone string `long:"timezone" short:"t" description:"The default time zone" env:"GOTIME_DEFAULT_TIMEZONE" default:"UTC"`
}
If you want to know what you can do with go-flags
, open the file ./restapi/server.go
and examine the Server struct there, and compare its contents to what you see when you type timeofday-server --help
. You can learn a lot by reading even the generated source code. As always, // DO NOT EDIT THIS FILE
.
Next, go into configure_timeofday.go
, and find the function configureFlags
. This, unsurprisingly, is where this feature is supposed to go.
We've already imported the timeofday
package, so we have access to our new Timezone type. Right above configureFlags
, let's create an instance of this struct and populate it with defaults:
var Timezone = timeofday.Timezone{
Timezone: "UTC",
}
See the comment in configureFlags? Note that swag
package? You'll have to add it to the imports. It should be already present, as it came with the rest of the swagger installation. Just add:
swag "github.com/go-openapi/swag"
And now modify configureFlags()
:
func configureFlags(api *operations.TimeofdayAPI) {
api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{
swag.CommandLineOptionsGroup{
ShortDescription: "Time Of Day Service Options",
Options: &Timezone,
},
}
}
See that ShortDescription
there? When you run the --help
option for the server, you'll see a section labeled "Application Options", and another labeled "Help Options". We're adding a new section, "Service Options", which will include our customizations. This conceptually allows us to distinguish between routine options of a microservice, and the specific options of this microservice.
Always distinguish between your framework and your business logic. (I've often seen this written as "always distinguish between execution exceptions and business exceptions," and it's great advice is similarly here.)
You can now build your server (go build ./cmd/timeofday-server/
), and run it (./timeofday-server --help
), and you'll see your new options. Of course, they don't do anything, we haven't modified your business logic!
The Context Problem
This is where most people have a problem. How do the values that now populate the Timezone struct make their way down to the handlers? There are a number of ways to do this. The "edit main.go
" people just make it a global variable available to the whole server, but I'm here to tell you doing so is sad and you should feel sad if you do it. What we have here, in our structure that holds our CLI options, is a context. How do we set the context?
The correct way is to modify the handlers so they have the context when they're called upon. The way we do that is via the oldest object-oriented technique of all time, one that dates all the way back to 1964 and the invention of Lisp: closures. A closure wraps one or more functions in an environment (a collection of variables outside those functions), and preserves handles to those variables even when those functions are passed out of the environment as references. A garbage-collected language like Go makes this an especially powerful technique because it means that anything in the environment for which you don't keep handles will get collected, leaving only what matters.
So, let's do it. Remember these lines in configure_timeofday.go
, from way back?
api.TestGetHandler = operations.TestGetHandlerFunc(func(params operations.TestGetParams) middleware.Responder {
return middleware.NotImplemented("operation .TestGet has not yet been implemented")
})
See that function that actually gets passed to TestHandlerGetFunc()? It's anonymous. We broke it out and gave it a name and stuff and filled it out with business logic and made it work. We're going to go back and replace those lines, again, so they look like this:
api.TimeGetHandler = operations.TimeGetHandlerFunc(timeofday.GetTime(&Timezone))
api.TimePostHandler = operations.TimePostHandlerFunc(timeofday.PostTime(&Timezone))
Those are no longer references to functions. They're function calls! What do those functions return? Well, we know TimeGetHandlerFunc() is expecting a reference to a function, so that function call had better return a reference to a function.
And indeed it does:
func GetTime(timezone *Timezone) func(operations.TimeGetParams) middleware.Responder{
defaultTZ := timezone.Timezone
// Here's the function we return:
return func(params operations.TimeGetParams) middleware.Responder {
// Everything else is the same... except we need *two* levels of
// closing } at the end!
Now, instead of returning a function defined at compile time, we returned a function reference that is finalized when GetTime() is called, and it now holds a permanent reference to our Timezone object. Do the same thing for PostTime
.
There's one more thing we have to do. We've moved our default timezone to the configure_timeofday.go
file, so we don't need it here anymore:
func getTimeOfDay(tz *string) (*string, error) {
t := time.Now()
utc, err := time.LoadLocation(*tz)
if err != nil {
return nil, errors.New(fmt.Sprintf("Time zone not found: %s", *tz))
}
thetime := t.In(utc).String()
return &thetime, nil
}
And that's it. That's everything. You can add all the command line arguments you want, and only preserve the fields that are relevant to the particular handler you're going to invoke.
You can now build and run the server, but with a command line:
$ go build ./cmd/timeofday-server/
$ ./timeofday-server --port=8080 --timezone="America/Los_Angeles"
And test it with curl:
$ curl 'http://localhost:8020/timeofday/v1/time?timezone=America/New_York'
{"timeofday":"2018-03-30 23:44:47.701895604 -0400 EDT"}
$ curl 'http://localhost:8020/timeofday/v1/time'
{"timeofday":"2018-03-30 20:44:54.525313806 -0700 PDT"}
Note that the default timezone is now PDT, or Pacific Daily Time, which corresponds to the America/Los_Angeles entry in the database in late March.
And that's how you add command line arguments to Swagger servers correctly without exposing your CLI settings to every other function in your server. If you want to see the entirety of the source code, the advanced version on the repository has it all.