From bee150dcad9f03662068b525113e0f3d2e6e484b Mon Sep 17 00:00:00 2001 From: "Elf M. Sternberg" Date: Fri, 30 Mar 2018 19:05:11 -0700 Subject: [PATCH] Keeping track of the new shit. --- DESCRIPION.md | 2 + README.md | 28 +++ docs/Part_01.md | 227 ++++++++++++++++++++++++ docs/Part_02.md | 148 +++++++++++++++ timeofday/{timeofday.go => handlers.go} | 0 5 files changed, 405 insertions(+) create mode 100644 DESCRIPION.md create mode 100644 README.md create mode 100644 docs/Part_01.md create mode 100644 docs/Part_02.md rename timeofday/{timeofday.go => handlers.go} (100%) diff --git a/DESCRIPION.md b/DESCRIPION.md new file mode 100644 index 0000000..53813b2 --- /dev/null +++ b/DESCRIPION.md @@ -0,0 +1,2 @@ +A simple Swagger example tutorial, but with a twist: explaining how to +get new CLI arguments down to your handlers. diff --git a/README.md b/README.md new file mode 100644 index 0000000..314124c --- /dev/null +++ b/README.md @@ -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. diff --git a/docs/Part_01.md b/docs/Part_01.md new file mode 100644 index 0000000..3beafe8 --- /dev/null +++ b/docs/Part_01.md @@ -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: + +<>= +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**. + +<>= +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: + +<>= +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: "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: + +<> + +<> + +<> +@ + +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. diff --git a/docs/Part_02.md b/docs/Part_02.md new file mode 100644 index 0000000..4b2ee5f --- /dev/null +++ b/docs/Part_02.md @@ -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: + +<>= +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: + +<>= + 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. + +<>= +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: + +<>= + 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: + +<>= + 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) diff --git a/timeofday/timeofday.go b/timeofday/handlers.go similarity index 100% rename from timeofday/timeofday.go rename to timeofday/handlers.go