Merge branch 'release/0.6.0'
* release/0.6.0: Documentation done, and Readme annotated with the appropriate links. Documentation done, and Readme annotated with the appropriate links. Finishing up the documentation. Next step: Blogging.
This commit is contained in:
commit
65e2fb2ef8
|
@ -6,8 +6,10 @@ single parameter either via GET or POST to specify the timezone from
|
||||||
which the client wants the time.
|
which the client wants the time.
|
||||||
|
|
||||||
This repository exists as a supplement to my tutorial,
|
This repository exists as a supplement to my tutorial,
|
||||||
[Adding Command Line Arguments to Go-Swagger](TK:), in which I do
|
[Adding Command Line Arguments to Go Swagger Microservices](http://www.elfsternberg.com/2018/03/30/writing-microservice-swagger-part-3-adding-command-line-arguments/),
|
||||||
exactly that.
|
in which I show how to do exactly that, by providing a dynamic way to
|
||||||
|
configure the default timezone at server start-up, via the CLI or an
|
||||||
|
environment variable.
|
||||||
|
|
||||||
# Status
|
# Status
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,23 @@ But if you're writing microservices for enterprise customers, yes, you
|
||||||
should use Go, and yes, you should use OpenAPI and Swagger. So here's
|
should use Go, and yes, you should use OpenAPI and Swagger. So here's
|
||||||
how it's done.
|
how it's done.
|
||||||
|
|
||||||
|
All of the files for this tutorial are available from the
|
||||||
|
elfsternberg/go-swagger-tutorial repo at github. There are *two* phases
|
||||||
|
to this tutorial, and the first phase is the base Go Swagger
|
||||||
|
implementation. I strongly recommend that if you're going to check out
|
||||||
|
the source code in its entirety, that you start with the
|
||||||
|
[Basic Version](https://github.com/elfsternberg/go-swagger-tutorial/tree/0.2.0),
|
||||||
|
and only check out the
|
||||||
|
[Advanced version](https://github.com/elfsternberg/go-swagger-tutorial/tree/0.4.0)
|
||||||
|
when you get to Part 3.
|
||||||
|
|
||||||
|
Just be aware that if you see stuff that looks like `<<this>>`, or a
|
||||||
|
single `@` alone on a line, that's just part of my code layout; do *not*
|
||||||
|
include those in your source code, they're not part of Go or Swagger.
|
||||||
|
Sorry about that.
|
||||||
|
|
||||||
|
## Go Swagger!
|
||||||
|
|
||||||
[Swagger](https://swagger.io/) is a specification that describes the
|
[Swagger](https://swagger.io/) is a specification that describes the
|
||||||
ndpoints for a webserver's API, usually a REST-based API. HTTP uses
|
ndpoints for a webserver's API, usually a REST-based API. HTTP uses
|
||||||
verbs (GET, PUT, POST, DELETE) and endpoints (/like/this) to describe
|
verbs (GET, PUT, POST, DELETE) and endpoints (/like/this) to describe
|
||||||
|
@ -36,12 +53,12 @@ functions with your business logic.
|
||||||
There are three things that are your responsibility:
|
There are three things that are your responsibility:
|
||||||
|
|
||||||
1. Write the specification that describes *exactly* what the server
|
1. Write the specification that describes *exactly* what the server
|
||||||
accepts as requests and returns as responses.
|
accepts as requests and returns as responses, and generate a server from
|
||||||
|
this specification.
|
||||||
|
|
||||||
2. Write the business logic.
|
2. Write the business logic.
|
||||||
|
|
||||||
3. Glue the business logic into the server generated from the
|
3. Glue the business logic into the generated server.
|
||||||
specification.
|
|
||||||
|
|
||||||
In Go-Swagger, there is *exactly one* file in the generated code that
|
In Go-Swagger, there is *exactly one* file in the generated code that
|
||||||
you need to change. Every other file is labeled "DO NOT EDIT." This
|
you need to change. Every other file is labeled "DO NOT EDIT." This
|
||||||
|
@ -64,6 +81,7 @@ $ go get -u github.com/golang/dep/cmd/dep
|
||||||
$ go get -u github.com/go-swagger/go-swagger/cmd/swagger
|
$ go get -u github.com/go-swagger/go-swagger/cmd/swagger
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Initialization
|
## Initialization
|
||||||
|
|
||||||
Now you're going to create a new project. Do it in your src directory
|
Now you're going to create a new project. Do it in your src directory
|
||||||
|
@ -91,15 +109,18 @@ optional timezone as a JSON argument in the body of the POST.
|
||||||
|
|
||||||
First, let's version our API. You do that with Basepaths:
|
First, let's version our API. You do that with Basepaths:
|
||||||
|
|
||||||
|
```
|
||||||
<<version the API>>=
|
<<version the API>>=
|
||||||
basePath: /timeofday/v1
|
basePath: /timeofday/v1
|
||||||
@
|
@
|
||||||
|
```
|
||||||
|
|
||||||
Now that we have a base path that versions our API, we want to define
|
Now that we have a base path that versions our API, we want to define
|
||||||
our endpoint. The URL will ultimately be `/timeofday/v1/time`, and we
|
our endpoint. The URL will ultimately be `/timeofday/v1/time`, and we
|
||||||
want to handle both GET and POST requests, and our responses are going
|
want to handle both GET and POST requests, and our responses are going
|
||||||
to be **Success: Time of day** or **Timezone Not Found**.
|
to be **Success: Time of day** or **Timezone Not Found**.
|
||||||
|
|
||||||
|
```
|
||||||
<<define the paths>>=
|
<<define the paths>>=
|
||||||
paths:
|
paths:
|
||||||
/time:
|
/time:
|
||||||
|
@ -133,11 +154,13 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/NotFound"
|
$ref: "#/definitions/NotFound"
|
||||||
@
|
@
|
||||||
|
```
|
||||||
|
|
||||||
The `$ref` entries are a YAML thing for referring to something else.
|
The `$ref` entries are a YAML thing for referring to something else.
|
||||||
The octothorpe symbol `(#)` indicates "look in the current file. So
|
The octothorpe symbol `(#)` indicates "look in the current file." So
|
||||||
now we have to create those paths:
|
now we have to create those paths:
|
||||||
|
|
||||||
|
```
|
||||||
<<schemas>>=
|
<<schemas>>=
|
||||||
definitions:
|
definitions:
|
||||||
NotFound:
|
NotFound:
|
||||||
|
@ -158,6 +181,7 @@ definitions:
|
||||||
properties:
|
properties:
|
||||||
TimeOfDay: string
|
TimeOfDay: string
|
||||||
@
|
@
|
||||||
|
```
|
||||||
|
|
||||||
This is *really verbose*, but on the other hand it is *undeniably
|
This is *really verbose*, but on the other hand it is *undeniably
|
||||||
complete*: these are the things we take in, and the things we respond
|
complete*: these are the things we take in, and the things we respond
|
||||||
|
@ -165,6 +189,7 @@ with.
|
||||||
|
|
||||||
So now your file looks like this:
|
So now your file looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
<<swagger.yml>>=
|
<<swagger.yml>>=
|
||||||
swagger: "2.0"
|
swagger: "2.0"
|
||||||
info:
|
info:
|
||||||
|
@ -186,14 +211,19 @@ schemes:
|
||||||
|
|
||||||
<<define the paths>>
|
<<define the paths>>
|
||||||
@
|
@
|
||||||
|
```
|
||||||
|
|
||||||
Now that you have that, it's time to generate the server!
|
Now that you have that, it's time to generate the server!
|
||||||
|
|
||||||
`$ swagger generate server -f swagger.yml`
|
`$ swagger generate server -f swagger.yml`
|
||||||
|
|
||||||
It will spill out the actions it takes as it generates your new REST
|
It will spill out the actions it takes as it generates your new REST
|
||||||
server. **Do not** follow the advice at the end of the output.
|
server. **Do not** follow the advice at the end of the output. There's
|
||||||
There's a better way.
|
a better way. Use `dep`, which will automagically find all your
|
||||||
|
dependencies for you, download them to a project-specific `vendor/`
|
||||||
|
folder, and _lock_ the specific commit in the record so version creep
|
||||||
|
won't break your project in the future. `dep` has become even Google's
|
||||||
|
recommended dependency control mechanism. Just run:
|
||||||
|
|
||||||
`$ dep init`
|
`$ dep init`
|
||||||
|
|
||||||
|
@ -224,4 +254,4 @@ $ curl http://localhost:8082/timeofday/v1/time
|
||||||
Congratulations! You have a working REST server that does, well,
|
Congratulations! You have a working REST server that does, well,
|
||||||
nothing.
|
nothing.
|
||||||
|
|
||||||
For part two, we'll make our server actually do things.
|
For Part 2, we'll make our server actually do things.
|
||||||
|
|
|
@ -1,37 +1,47 @@
|
||||||
# Review of Part One
|
# Review of Part One
|
||||||
|
|
||||||
In [Part One of Go-Swagger](TK:), we generated a on OpenAPI 2.0 server
|
In
|
||||||
with REST endpoints. The server builds and responds to queries, but
|
[Part One of Go-Swagger](http://www.elfsternberg.com/2018/03/30/writing-microservice-swagger-part-1-specification/),
|
||||||
every valid query ends with "This feature has not yet been
|
we generated a on OpenAPI 2.0 server with REST endpoints. The server
|
||||||
implemented."
|
builds and responds to queries, but every valid query ends with "This
|
||||||
|
feature has not yet been implemented."
|
||||||
|
|
||||||
It's time to implement the feature.
|
It's time to implement the feature.
|
||||||
|
|
||||||
I want to emphasize that with Go Swagger there is *only* one generated
|
I want to emphasize that with Go Swagger there is *only* one generated
|
||||||
file you need to touch. Since our project is named `timezone`, the
|
file you need to touch. Since our project is named `timeofday`, the
|
||||||
file will be named `restapi/configure_timezone.go`. Our first step
|
file will be named `restapi/configure_timeofday.go`. Our first step
|
||||||
will be to break those "not implemented" functions out into their own
|
will be to break those "not implemented" functions out into their own
|
||||||
Go package. That package will be our business logic. The configure
|
Go package. That package will be our business logic. The configure
|
||||||
file and the business logic package will be the *only* things we
|
file and the business logic package will be the *only* things we
|
||||||
change.
|
change.
|
||||||
|
|
||||||
|
A reminder: The final source code for this project is available on
|
||||||
|
Github, however Parts One & Two deal with the most common
|
||||||
|
implementation, a server with hard-coded default values. For these
|
||||||
|
chapters, please consult
|
||||||
|
[that specific version of the code](https://github.com/elfsternberg/go-swagger-tutorial/tree/0.2.0).
|
||||||
|
|
||||||
## Break out the business logic
|
## Break out the business logic
|
||||||
|
|
||||||
Create a new folder in your project root and call it `timeofday`.
|
Create a new folder in your project root and call it `timeofday`.
|
||||||
|
|
||||||
Open up your editor and find the file `restapi/configure_timeofday.go`.
|
Open up your editor and find the file `restapi/configure_timeofday.go`.
|
||||||
In your `swagger.yml` file you created two endpoints and gave them each
|
In your `swagger.yml` file you created two endpoints and gave them each
|
||||||
an `operationId`: `TimekPost` and `TimeGet`. Inside
|
an `operationId`: `TimePost` and `TimeGet`. Inside
|
||||||
`configure_timeofday.go`, you should find two corresponding assignments
|
`configure_timeofday.go`, you should find two corresponding assignments
|
||||||
in the function `configureAPI()`: `TimeGetHandlerFunc` and
|
in the function `configureAPI()`: `TimeGetHandlerFunc` and
|
||||||
`ClockPostHandlerFunc`. Inside those function calls, you'll find
|
`TimePostHandlerFunc`. Inside those function calls, you'll find
|
||||||
anonymous functions.
|
anonymous functions.
|
||||||
|
|
||||||
I want you to take those anonymous functions, cut them out, and paste
|
I want you to take those anonymous functions, cut them out, and paste
|
||||||
them into a new file inside the `timeofday/` folder. You will also have
|
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
|
to create a package name and import any packages being used. Now your
|
||||||
file, which I've called `timeofday/handlers.go`, looks like this:
|
file, which I've called `timeofday/handlers.go`, looks like this (note
|
||||||
|
that you'll have to change your import paths as you're probably not
|
||||||
|
elfsternberg. Heck, _I'm_ probably not elfsternberg):
|
||||||
|
|
||||||
|
```
|
||||||
<<handlers.go before implementation>>=
|
<<handlers.go before implementation>>=
|
||||||
package timeofday
|
package timeofday
|
||||||
|
|
||||||
|
@ -48,15 +58,18 @@ func PostTime(params operations.TimePostParams) middleware.Responder {
|
||||||
return middleware.NotImplemented("operation .TimePost has not yet been implemented")
|
return middleware.NotImplemented("operation .TimePost has not yet been implemented")
|
||||||
}
|
}
|
||||||
@
|
@
|
||||||
|
```
|
||||||
|
|
||||||
And now go back to `restapi/configure_timeofday.go`, add
|
And now go back to `restapi/configure_timeofday.go`, add
|
||||||
`github.com/elfsternberg/timeofday/clock` to the imports, and change the
|
`github.com/elfsternberg/timeofday/clock` to the imports, and change the
|
||||||
handler lines to look like this:
|
handler lines to look like this:
|
||||||
|
|
||||||
|
```
|
||||||
<<configuration lines before implementation>>=
|
<<configuration lines before implementation>>=
|
||||||
api.TimeGetHandler = operations.TimeGetHandlerFunc(timeofday.GetTime)
|
api.TimeGetHandler = operations.TimeGetHandlerFunc(timeofday.GetTime)
|
||||||
api.TimePostHandler = operations.TimePostHandlerFunc(timeofday.PostTime)
|
api.TimePostHandler = operations.TimePostHandlerFunc(timeofday.PostTime)
|
||||||
@
|
@
|
||||||
|
```
|
||||||
|
|
||||||
## Implementation
|
## Implementation
|
||||||
|
|
||||||
|
@ -90,6 +103,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,
|
in the operations files, there are a methods for good and bad returns,
|
||||||
as we described in the swagger file.
|
as we described in the swagger file.
|
||||||
|
|
||||||
|
```
|
||||||
<<gettime implementation>>=
|
<<gettime implementation>>=
|
||||||
func GetTime(params operations.TimeGetParams) middleware.Responder {
|
func GetTime(params operations.TimeGetParams) middleware.Responder {
|
||||||
var tz *string = nil
|
var tz *string = nil
|
||||||
|
@ -101,6 +115,7 @@ func GetTime(params operations.TimeGetParams) middleware.Responder {
|
||||||
thetime, err := getTimeOfDay(params.Timezone)
|
thetime, err := getTimeOfDay(params.Timezone)
|
||||||
|
|
||||||
@
|
@
|
||||||
|
```
|
||||||
|
|
||||||
The first thing to notice here is the `params` field: we're getting a
|
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
|
customized, tightly bound object from the server. There's no hope of
|
||||||
|
@ -113,6 +128,7 @@ We then call a (thus far undefined) function called `getTimeOfDay`.
|
||||||
|
|
||||||
Let's deal with the error case:
|
Let's deal with the error case:
|
||||||
|
|
||||||
|
```
|
||||||
<<gettime implementation>>=
|
<<gettime implementation>>=
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return operations.NewTimeGetNotFound().WithPayload(
|
return operations.NewTimeGetNotFound().WithPayload(
|
||||||
|
@ -122,6 +138,7 @@ Let's deal with the error case:
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@
|
@
|
||||||
|
```
|
||||||
|
|
||||||
That's a lot of references. We have a model, an operation, and what's
|
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
|
that "swag" thing? In order to satisfy Swagger's strictness, we use
|
||||||
|
@ -134,6 +151,7 @@ the response with content.
|
||||||
|
|
||||||
The good path is similar:
|
The good path is similar:
|
||||||
|
|
||||||
|
```
|
||||||
<<gettime implementation>>=
|
<<gettime implementation>>=
|
||||||
return operations.NewClockGetOK().WithPayload(
|
return operations.NewClockGetOK().WithPayload(
|
||||||
&models.Timeofday{
|
&models.Timeofday{
|
||||||
|
@ -141,8 +159,64 @@ The good path is similar:
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@
|
@
|
||||||
|
```
|
||||||
|
|
||||||
Now might be a good time to go look in `models/` and `/restapi/options`,
|
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
|
to see what's available to you. You'll need to do so anyway, because
|
||||||
unless you go to the
|
unless you go to the
|
||||||
[git repository](https://github.com/elfsternberg/go-swagger-tutorial)
|
[git repository](https://github.com/elfsternberg/go-swagger-tutorial/tree/0.2.0)
|
||||||
|
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! Just a reminder, a working version of this server is
|
||||||
|
available under the "0.2.0" tag
|
||||||
|
[at the repo](https://github.com/elfsternberg/go-swagger-tutorial/tree/0.2.0).
|
||||||
|
|
||||||
|
On to [Part 3](TK)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,235 @@
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue