236 lines
8.8 KiB
Markdown
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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|