go-swagger-tutorial/docs/Part_03.md

236 lines
8.8 KiB
Markdown

The first two parts of my swagger tutorial
[[Part 1](http://www.elfsternberg.com/2018/03/30/writing-microservice-swagger-part-1-specification/),
[Part 2](http://www.elfsternberg.com/2018/03/30/writing-microservice-swagger-part-2-business-logic/)]
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](http://www.elfsternberg.com/2018/03/30/writing-microservice-swagger-part-2-business-logic/)
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](https://github.com/go-openapi/kvstore/blob/master/cmd/kvstored/main.go#L50-L57),
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](https://golang.org/pkg/reflect/) 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 1964 and the invention of Lisp:
*[closures](https://tour.golang.org/moretypes/25)*. 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 referece to a function, so 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 entirely of the source code, the
[advanced version on the repository](https://github.com/elfsternberg/go-swagger-tutorial/tree/0.4.0)
has it all.