Keeping track of the new shit.
This commit is contained in:
parent
7ed37720fc
commit
bee150dcad
|
@ -0,0 +1,2 @@
|
||||||
|
A simple Swagger example tutorial, but with a twist: explaining how to
|
||||||
|
get new CLI arguments down to your handlers.
|
|
@ -0,0 +1,28 @@
|
||||||
|
# TIME-OF-DAY
|
||||||
|
|
||||||
|
TimeOfDay is a dead-simple microservice written in Go that, when pinged
|
||||||
|
at the correct URL, returns the time of day. The server can take a
|
||||||
|
single parameter either via GET or POST to specify the timezone from
|
||||||
|
which the client wants the time.
|
||||||
|
|
||||||
|
This repository exists as a supplement to my tutorial,
|
||||||
|
[Adding Command Line Arguments to Go-Swagger](TK:), in which I do
|
||||||
|
exactly that.
|
||||||
|
|
||||||
|
# Status
|
||||||
|
|
||||||
|
This project is **complete**. No future work will be done on it.
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
Apache 2.0. See the accompanying LICENSE file in this directory.
|
||||||
|
|
||||||
|
# Warranty
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||||
|
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@ -0,0 +1,227 @@
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|
[${WORK}](http://www.splunk.com) has me writing microservices in Go,
|
||||||
|
using OpenAPI 2.0 / Swagger. While I'm not a fan of Go (that's a bit of
|
||||||
|
an understatement) I get why Go is popular with enterprise managers, it
|
||||||
|
does exactly what it says it does. It's syntactically hideous. I'm
|
||||||
|
perfectly happy taking a paycheck to write in it, and I'm pretty good at
|
||||||
|
it already. I just wouldn't choose it for a personal project.
|
||||||
|
|
||||||
|
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
|
||||||
|
how it's done.
|
||||||
|
|
||||||
|
[Swagger](https://swagger.io/) is a specification that describes the
|
||||||
|
ndpoints for a webserver's API, usually a REST-based API. HTTP uses
|
||||||
|
verbs (GET, PUT, POST, DELETE) and endpoints (/like/this) to describe
|
||||||
|
things your service handles and the operations that can be performed
|
||||||
|
against it.
|
||||||
|
|
||||||
|
Swagger starts with a **file**, written in JSON or YAML, that names
|
||||||
|
each and every endpoint, the verbs that endpoint responds to, the
|
||||||
|
parameters that endpoint requires and takes optionally, and the
|
||||||
|
possible responses, with type information for every field in the
|
||||||
|
inputs and outputs.
|
||||||
|
|
||||||
|
Swagger *tooling* then takes that file and generates a server ready to
|
||||||
|
handle all those transactions. The parameters specified in the
|
||||||
|
specification file are turned into function calls and populated with
|
||||||
|
"Not implemented" as the only thing they return.
|
||||||
|
|
||||||
|
## Your job
|
||||||
|
|
||||||
|
In short, for a basic microservice, it's your job to replace those
|
||||||
|
functions with your business logic.
|
||||||
|
|
||||||
|
There are three things that are your responsibility:
|
||||||
|
|
||||||
|
1. Write the specification that describes *exactly* what the server
|
||||||
|
accepts as requests and returns as responses.
|
||||||
|
|
||||||
|
2. Write the business logic.
|
||||||
|
|
||||||
|
3. Glue the business logic into the server generated from the
|
||||||
|
specification.
|
||||||
|
|
||||||
|
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
|
||||||
|
one file, called `configure_project.go`, has a top line that says "This
|
||||||
|
file is safe to edit, and will not be replaced if you re-run swagger."
|
||||||
|
That *exactly one* file should be the only one you ever need to change.
|
||||||
|
|
||||||
|
## The setup
|
||||||
|
|
||||||
|
You'll need Go. I'm not going to go into setting up Go on your system;
|
||||||
|
there are
|
||||||
|
[perfectly adequate guides elsewhere](https://golang.org/doc/install).
|
||||||
|
You will need to install `swagger` and `dep`.
|
||||||
|
|
||||||
|
Once you've set up your Go environment (set up $GOPATH and $PATH), you
|
||||||
|
can just:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get -u github.com/golang/dep/cmd/dep
|
||||||
|
$ go get -u github.com/go-swagger/go-swagger/cmd/swagger
|
||||||
|
```
|
||||||
|
|
||||||
|
## Initialization
|
||||||
|
|
||||||
|
Now you're going to create a new project. Do it in your src directory
|
||||||
|
somewhere, under your $GOPATH.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ mkdir project timeofday
|
||||||
|
$ cd timeofday
|
||||||
|
$ git init && git commit --allow-empty -m "Init commit: Swagger Time of Day."
|
||||||
|
$ swagger init spec --format=yaml --title="Timeofday" --description="A silly time-of-day microservice"
|
||||||
|
```
|
||||||
|
|
||||||
|
You will now find a new swagger file in your project directory. If
|
||||||
|
you open it up, you'll see short header describing the basic features
|
||||||
|
Swagger needs to understand your project.
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
Swagger works with **operations**, which is a combination of a verb
|
||||||
|
and an endpoint. We're going to have two operations which do the same
|
||||||
|
thing: return the time of day. The two operations use the same
|
||||||
|
endpoint, but different verbs: GET and POST. The GET argument takes
|
||||||
|
an optional timezone as a search option; the POST argument takes an
|
||||||
|
optional timezone as a JSON argument in the body of the POST.
|
||||||
|
|
||||||
|
First, let's version our API. You do that with Basepaths:
|
||||||
|
|
||||||
|
<<version the API>>=
|
||||||
|
basePath: /timeofday/v1
|
||||||
|
@
|
||||||
|
|
||||||
|
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
|
||||||
|
want to handle both GET and POST requests, and our responses are going
|
||||||
|
to be **Success: Time of day** or **Timezone Not Found**.
|
||||||
|
|
||||||
|
<<define the paths>>=
|
||||||
|
paths:
|
||||||
|
/time:
|
||||||
|
get:
|
||||||
|
operationId: "GetTime"
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: Timezone
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
minLength: 3
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/TimeOfDay"
|
||||||
|
404:
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/NotFound"
|
||||||
|
post:
|
||||||
|
operationId: "PostTime"
|
||||||
|
parameters:
|
||||||
|
- in: body
|
||||||
|
name: Timezone
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Timezone"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/TimeOfDay"
|
||||||
|
404:
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/NotFound"
|
||||||
|
@
|
||||||
|
|
||||||
|
The `$ref` entries are a YAML thing for referring to something else.
|
||||||
|
The octothorpe symbol `(#)` indicates "look in the current file. So
|
||||||
|
now we have to create those paths:
|
||||||
|
|
||||||
|
<<schemas>>=
|
||||||
|
definitions:
|
||||||
|
NotFound:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: integer
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
Timezone:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
Timezone:
|
||||||
|
type: string
|
||||||
|
minLength: 3
|
||||||
|
TimeOfDay:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
TimeOfDay: string
|
||||||
|
@
|
||||||
|
|
||||||
|
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
|
||||||
|
with.
|
||||||
|
|
||||||
|
So now your file looks like this:
|
||||||
|
|
||||||
|
<<swagger.yml>>=
|
||||||
|
swagger: "2.0"
|
||||||
|
info:
|
||||||
|
version: 0.1.0
|
||||||
|
title: timeofday
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
schemes:
|
||||||
|
- http
|
||||||
|
|
||||||
|
# Everything above this line was generated by the swagger command.
|
||||||
|
# Everything below this line you have to add:
|
||||||
|
|
||||||
|
<<version the API>>
|
||||||
|
|
||||||
|
<<schemas>>
|
||||||
|
|
||||||
|
<<define the paths>>
|
||||||
|
@
|
||||||
|
|
||||||
|
Now that you have that, it's time to generate the server!
|
||||||
|
|
||||||
|
`$ swagger generate server -f swagger.yml`
|
||||||
|
|
||||||
|
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.
|
||||||
|
There's a better way.
|
||||||
|
|
||||||
|
`$ dep init`
|
||||||
|
|
||||||
|
Dependency management in Go is a bit of a mess, but the accepted
|
||||||
|
solution now is to use `dep` rather than `go get`. This creates a
|
||||||
|
pair of files, one describing the Go packages that your file uses, and
|
||||||
|
one describing the exact *versions* of those packages that you last
|
||||||
|
downloaded and used in the `./vendor/` directory under your project
|
||||||
|
root.
|
||||||
|
|
||||||
|
Now you can build the server:
|
||||||
|
|
||||||
|
`$ go build ./cmd/timeofday-server/`
|
||||||
|
|
||||||
|
And then you can run it. Feel free to change the port number:
|
||||||
|
|
||||||
|
`$ ./timeofday-server --port=8082`
|
||||||
|
|
||||||
|
You can now tickle the server:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl http://localhost:8082/
|
||||||
|
{"code":404,"message":"path / was not found"}
|
||||||
|
$ curl http://localhost:8082/timeofday/v1/time
|
||||||
|
" function .GetTime is not implemented"
|
||||||
|
```
|
||||||
|
|
||||||
|
Congratulations! You have a working REST server that does, well,
|
||||||
|
nothing.
|
||||||
|
|
||||||
|
For part two, we'll make our server actually do things.
|
|
@ -0,0 +1,148 @@
|
||||||
|
# Review of Part One
|
||||||
|
|
||||||
|
In [Part One of Go-Swagger](TK:), we generated a on OpenAPI 2.0 server
|
||||||
|
with REST endpoints. The server 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.
|
||||||
|
|
||||||
|
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 will be named `restapi/configure_timezone.go`. Our first step
|
||||||
|
will be to break those "not implemented" functions out into their own
|
||||||
|
Go package. That package will be our business logic. The configure
|
||||||
|
file and the business logic package will be the *only* things we
|
||||||
|
change.
|
||||||
|
|
||||||
|
## Break out the business logic
|
||||||
|
|
||||||
|
Create a new folder in your project root and call it `timeofday`.
|
||||||
|
|
||||||
|
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
|
||||||
|
an `operationId`: `TimekPost` and `TimeGet`. Inside
|
||||||
|
`configure_timeofday.go`, you should find two corresponding assignments
|
||||||
|
in the function `configureAPI()`: `TimeGetHandlerFunc` and
|
||||||
|
`ClockPostHandlerFunc`. Inside those function calls, you'll find
|
||||||
|
anonymous functions.
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
import(
|
||||||
|
"github.com/go-openapi/runtime/middleware"
|
||||||
|
"github.com/elfsternberg/timeofday/restapi/operations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetTime(params operations.TimeGetParams) middleware.Responder {
|
||||||
|
return middleware.NotImplemented("operation .TimeGet has not yet been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Believe it or not, you've now done everything you need to do except the
|
||||||
|
business logic. We're going to honor the point of OpenAPI and the `//
|
||||||
|
DO NOT EDIT`` comments, and not modify anything exceept the contents of
|
||||||
|
our handler.
|
||||||
|
|
||||||
|
To understand our code, though, we're going to have to *read* some of
|
||||||
|
those files. Let's go look at `/models`. In here, you'll find the
|
||||||
|
schemas you outlined in the `swagger.yml` file turned into source code.
|
||||||
|
If you open one, like many files generated by Swagger, you'll see it
|
||||||
|
reads `// DO NOT EDIT`. But then there's that function there,
|
||||||
|
`Validate()`. What if you want to do advanced validation for custom
|
||||||
|
patterns or inter-field relations not covered by Swagger's validators?
|
||||||
|
|
||||||
|
Well, you'll have to edit this file. And figure out how to live with
|
||||||
|
it. We're not going to do that here. This exercise is about *not*
|
||||||
|
editing those files. But we can see, for example, that the `Timezone`
|
||||||
|
object has a field, `Timezone.Timezone`, which is a string, and which
|
||||||
|
has to be at least three bytes long.
|
||||||
|
|
||||||
|
The other thing you'll have to look at is the `restapi/operations`
|
||||||
|
folder. In here you'll find GET and POST operations, the parameters
|
||||||
|
they accept, the responses they deliver, and lots of functions only
|
||||||
|
Swagger cares about. But there are a few we care about.
|
||||||
|
|
||||||
|
Here's how we craft the GET response. Inside `handlers.go`, you're
|
||||||
|
going to need to extract the requested timezone, get the time of day,
|
||||||
|
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
|
||||||
|
|
||||||
|
if (params.Timezone != nil) {
|
||||||
|
tz = params.Timezone
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
abstraction here. The next is that we made the Timezone input optional,
|
||||||
|
so here we have to check if it's `nil` or not. if it isn't, we need to
|
||||||
|
set it. We do this here because we need to *cast* params.Timezone into
|
||||||
|
a pointer to a string, because Go is weird about types.
|
||||||
|
|
||||||
|
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(
|
||||||
|
&models.ErrorResponse {
|
||||||
|
int32(operations.TimeGetNotFoundCode),
|
||||||
|
swag.String(fmt.Sprintf("%s", err)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@
|
||||||
|
|
||||||
|
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
|
||||||
|
only what Swagger offers: for our 404 case, we didn't find the timezone
|
||||||
|
requested, so we're returning the ErrorResponse model populated with a
|
||||||
|
numeric code and a string, extracted via `fmt`, from the err returned
|
||||||
|
from our time function. The 404 case for get is called, yes,
|
||||||
|
`NewClockGetNotFound`, and then `WithPayload()` decorates the body of
|
||||||
|
the response with content.
|
||||||
|
|
||||||
|
The good path is similar:
|
||||||
|
|
||||||
|
<<gettime implementation>>=
|
||||||
|
return operations.NewClockGetOK().WithPayload(
|
||||||
|
&models.Timeofday{
|
||||||
|
Timeofday: *thetime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@
|
||||||
|
|
||||||
|
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)
|
Loading…
Reference in New Issue