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