Finishing up the documentation. Next step: Blogging.
This commit is contained in:
parent
d51670d7ad
commit
406fee5458
|
@ -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:).
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue