Keeping track of the new shit.

This commit is contained in:
Elf M. Sternberg 2018-03-30 19:05:11 -07:00
parent 7ed37720fc
commit bee150dcad
5 changed files with 405 additions and 0 deletions

2
DESCRIPION.md Normal file
View File

@ -0,0 +1,2 @@
A simple Swagger example tutorial, but with a twist: explaining how to
get new CLI arguments down to your handlers.

28
README.md Normal file
View File

@ -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.

227
docs/Part_01.md Normal file
View File

@ -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.

148
docs/Part_02.md Normal file
View File

@ -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)