go-swagger-tutorial/docs/Part_03.md

203 lines
7.1 KiB
Markdown
Raw Normal View History

# Adding Command Line Arguments to the Swagger Server
The first two parts of my swagger tutorial 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 teach how to extend the
Command Line Arguments of a swagger project.
Nobody else, so far as I know, knows how to do this. One thing that I
emphasized in [Go Swagger Part 2](TK:) 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 ones 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 retrospection](TK:) features to encade 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:
```
<<timezone.go>>=
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:
```
<<add timezone to configure_timeofday.go>>=
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:
```
<<new import for swag>>=
swag "github.com/go-openapi/swag"
@
```
And now modify `configureFlags()`:
```
<<rewrite 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 1959 and the invention of Lisp: *closures*.
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 function, so so that function call had better return a
function.
And indeed it does.
```
func GetTime(timezone *Timezone) func(operations.TimeGetParams) middleware.Responder{
defaultTZ := timezone.Timezone
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 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.
And *that's* how you add command line arguments to Swagger servers
correctly.