Finishing up the documentation. Next step: Blogging.

This commit is contained in:
Elf M. Sternberg 2018-03-30 20:00:15 -07:00
parent d51670d7ad
commit 406fee5458
2 changed files with 263 additions and 0 deletions

View File

@ -32,6 +32,7 @@ them into a new file inside the `timeofday/` folder. You will also have
to create a package name and import any packages being used. Now your
file, which I've called `timeofday/handlers.go`, looks like this:
```
<<handlers.go before implementation>>=
package timeofday
@ -48,15 +49,18 @@ func PostTime(params operations.TimePostParams) middleware.Responder {
return middleware.NotImplemented("operation .TimePost has not yet been implemented")
}
@
```
And now go back to `restapi/configure_timeofday.go`, add
`github.com/elfsternberg/timeofday/clock` to the imports, and change the
handler lines to look like this:
```
<<configuration lines before implementation>>=
api.TimeGetHandler = operations.TimeGetHandlerFunc(timeofday.GetTime)
api.TimePostHandler = operations.TimePostHandlerFunc(timeofday.PostTime)
@
```
## Implementation
@ -90,6 +94,7 @@ and then return either a success message or an error message. Looking
in the operations files, there are a methods for good and bad returns,
as we described in the swagger file.
```
<<gettime implementation>>=
func GetTime(params operations.TimeGetParams) middleware.Responder {
var tz *string = nil
@ -101,6 +106,7 @@ func GetTime(params operations.TimeGetParams) middleware.Responder {
thetime, err := getTimeOfDay(params.Timezone)
@
```
The first thing to notice here is the `params` field: we're getting a
customized, tightly bound object from the server. There's no hope of
@ -113,6 +119,7 @@ We then call a (thus far undefined) function called `getTimeOfDay`.
Let's deal with the error case:
```
<<gettime implementation>>=
if err != nil {
return operations.NewTimeGetNotFound().WithPayload(
@ -122,6 +129,7 @@ Let's deal with the error case:
})
}
@
```
That's a lot of references. We have a model, an operation, and what's
that "swag" thing? In order to satisfy Swagger's strictness, we use
@ -134,6 +142,7 @@ the response with content.
The good path is similar:
```
<<gettime implementation>>=
return operations.NewClockGetOK().WithPayload(
&models.Timeofday{
@ -141,8 +150,60 @@ The good path is similar:
})
}
@
```
Now might be a good time to go look in `models/` and `/restapi/options`,
to see what's available to you. You'll need to do so anyway, because
unless you go to the
[git repository](https://github.com/elfsternberg/go-swagger-tutorial)
and cheat, I'm going to leave it up to you to implement the PostTime().
There's still one thing missing, though: the actual time of day. We'll
need a default, and we'll need to test to see if the default is needed.
The implementation is straightforward:
```
<<timeofday function>>=
func getTimeOfDay(tz *string) (*string, error) {
defaultTZ := "UTC"
t := time.Now()
if tz == nil {
tz = &defaultTZ
}
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
}
@
```
Now, if you've written everything correctly, and the compiler admits
that you have (or you can cheat and download the 0.2.0-tagged version
from the the repo), you'll be able to build, compile, and run the
server, and see it working:
```
$ go build ./cmd/timeofday-server/
$ ./timeofday-server --port=8080
```
And then test it with curl:
```
$ curl 'http://localhost:8020/timeofday/v1/time'
{"timeofday":"2018-03-31 02:57:48.814683375 +0000 UTC"}
$ curl 'http://localhost:8020/timeofday/v1/time?timezone=UTC'
{"timeofday":"2018-03-31 02:57:50.443200906 +0000 UTC"}
$ curl 'http://localhost:8020/timeofday/v1/time?timezone=America/Los_Angeles'
{"timeofday":"2018-03-30 19:57:59.886650128 -0700 PDT"}
```
And that's the end of Part 2. If you've gotten this far,
congratulations! On to [Part 3](TK:).

202
docs/Part_03.md Normal file
View File

@ -0,0 +1,202 @@
# 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.