Swagger with Go Part 3 - Stratoscale/swagger
In the previous post, I gave an intro for go-swagger, a tool that generates Go code from swagger files. In this post, we will see Stratoscale’s version of go-swagger, available as open source.
Intro
Stratoscale’s swagger (or Stratoscale/swagger) is a slightly modified go-swagger. It takes advantage of the fact that swagger exposes a flag to run it with custom templates. Those template files are the ones that are being used to generate the Go code. Not all of the files can be modified - but it is a good thing - if you change less things, you can easily upgrade go-swagger versions, which include bug fixes and improvements.
Usage
Since Stratoscale/swagger is using the actual swagger
command, just with custom template files, we found that
the easiest way to provide a command line tool, is with a docker container - one that contains both the
swagger command, our own templates and runs the swagger command with the appropriate flags.
It is also one of the ways to run swagger from the
go-swagger install docs.
I personally prefer to run tools with docker container and not with downloaded binaries - It is easier in scripts and build systems - once you have docker running on a given machine, you don’t need to install anything else and scripts just work (If the image does not exists, the docker engine will pull it automatically).
To install Stratoscale/swagger use the following bash alias:
$ alias swagger='docker run --rm -e GOPATH=${GOPATH}:/go -v $(pwd):$(pwd) -w $(pwd) -u $(id -u):$(id -u) stratoscale/swagger:v1.0.14'
It is similar to the one described in go-swagger docs, but has the following changes:
- Uses Stratoscale’s container image
- Uses a versioned image - to maintain reproducibility of builds.
- Removes the unnessecary
-it
flags (stand for interactive and TTY docker run) - Mounts
$(pwd)
and not${HOME}
which will mount less data into the container and will work if you are not under${HOME}
. - Adds
-u
which changes the user in the container, so the generated files won’t be owned by theroot
user.
After defining the alias, a swagger
command will be available.
To test that it works:
$ swagger version
version: 0.14.0
commit: 25e637c5028dee7baf8cdf5d172ccb28cb8e5c3e
This command assumes your project is in the GOPATH
and you are currently in the directory that has the
swagger.yaml
file.
- Check out the Docker Hub tags page for the latest version
- You can add the alias command line to your
~/.bashrc
file in order to make it available any time you get a bash shell.
Features
So what is the difference between Stratoscale/swagger and go-swagger? We tried to approach some of the pain points in go-swagger described in the previous post. They are described below.
Improved Server Template
As described in the previous post, one of the week points in go-swagger, in my
opinion, is the generated restapi/configure_*.go
file, that is autogenerated only once on the first use.
We took inspiration from the protobuf Go implementation. There, when you run a server, The generated code takes an object that should implement the service functionality.
Our solution was to modify the restapi/configure_*.go
file. We introduced a number of changes, described below:
1. Always Generated
Our configure file is re-generated every time swagger
is called, and it should not be modified.
2. Expose http.Handler
As described in the previous post, go-swagger
gives a fully functional
server command. But customizing it is hard, and specially getting the http.Handler
to run with your own code.
Our restapi
package exposes an restapi.Handler
function, that should be called with restapi.Config
struct.
This struct contains the server configuration - the managers that implement all the server functionality (see
next section).
The function returns a standard http.Handler
that can be used as you wish - wrap it with middlewares and serve it with
whatever go server you like.
3. Expose Service Interfaces
We added interfaces section, that are exposed from the restapi
package. Each swagger
tag is exposed through
a <tag-name>API
interface, and contains all the operations that belong to this tag.
Using tags to categorize operations is a common methodology in the Open API auto generated code, and it is also
the practice in go-swagger
. This enables the user separate different logic entities in the server, we call those
logical entities “managers”.
In the example
we can see the exposed PetAPI
and StoreAPI
interfaces. Here is the PetAPI
interface:
type PetAPI interface {
PetCreate(ctx context.Context, params pet.PetCreateParams) middleware.Responder
PetDelete(ctx context.Context, params pet.PetDeleteParams) middleware.Responder
PetGet(ctx context.Context, params pet.PetGetParams) middleware.Responder
PetList(ctx context.Context, params pet.PetListParams) middleware.Responder
PetUpdate(ctx context.Context, params pet.PetUpdateParams) middleware.Responder
}
Those interfaces are defined as fields in the restapi.Config
struct that configures the server http.Handler
.
This enables testing the http handler routing and middleware without actually invoking the business logic.
For example, in the main_test.go we can see testing of the http handler with mocking of all the “managers”. Those tests specify the expected behavior for authentication and authorization since all the business logic is being mocked.
It also enables the “managers” to be encapsulated, they only implement the interface and can be tested separately. What in go-swagger is part of a function that statically sets functions to a router, is here a an encapsulated entity that is injected to the router config.
4. Usage of context.Context
The way go-swagger injects authentication tokens to the operation function by adding a second argument called
principal
, which is also not that well documented.
Our generated operation functions receive a context.Context
as their first argument. In runtime, this context will be
the incoming request context. Instead of passing the principal object as an argument to the operation function we use
the context object with the restapi.AuthKey
key.
This context can also be used with middlewares - they can inject values to the context and read it when necessary in the operation function’s body.
Take care not to abuse the context function, and use it only for “contextual” data.
Improved Client Template
As described in the previous post, the go-swagger generated client code is hard to consume, customize and it exposes non-standard options. We decided to change its template too, and introduce the following changes:
1. Creating a New Client
We changed the client “generator” signature to only be a client.New
function, that returns a new client, and it
receives a client.Config
with optional customizable fields:
- URL (of standard type
*url.URL
) - sets client to a custom endpoint, the default one is the endpoint that is set in the swagger file. - Transport (of standard type
http.RoundTripper
) - To enable custom client side middlewares, such as authentication, logging, tracing etc.
2. Expose Client Interfaces
Exposing interfaces for clients is important. It makes the consumption of such client easier.
The AWS SDK is a good example for that, each service exposes a client and an interface that implement
the client methods.
For example, here is the ec2.New()
,
function that returns an *ec2.EC2
object - the client with the EC2 API. But the SDK also includes the package
ec2iface
with the ec2iface.EC2API
which is just the interface that the same client implements.
go-swagger generates a client object for each service tag. For example, the pet-store client contains two fields for the pet-store services:
type SwaggerPetstore struct {
Pet *pet.Client
Store *store.Client
...
}
Each of those services is defined on its own package. We added to each package an API
interface, which defines
the service functionality and should be used similarly to the described above. For example, the client/pet
package
now contains the following interface:
type API interface {
// PetCreate adds a new pet to the store
PetCreate(ctx context.Context, params *PetCreateParams) (*PetCreateCreated, error)
// PetDelete deletes a pet
PetDelete(ctx context.Context, params *PetDeleteParams) (*PetDeleteNoContent, error)
// PetGet gets pet by it s ID
PetGet(ctx context.Context, params *PetGetParams) (*PetGetOK, error)
// PetList lists pets
PetList(ctx context.Context, params *PetListParams) (*PetListOK, error)
// PetUpdate updates an existing pet
PetUpdate(ctx context.Context, params *PetUpdateParams) (*PetUpdateCreated, error)
}
Additionally, we generate with mockery (A tool for generating a mock for a given
interface) the type MockAPI
which is a mock for the API
interface. So it could be
used in tests without the need to generate it by the consumer.
Client Usage Example
Let’s define a type that uses the pet API of our pet store.
type PetUser struct {
Pet pet.API
}
func (pu *PetUser) Duplicate(ctx context.Context) error {
pets, err := pu.Pet.PetList(ctx, nil)
if err != nil {
return fmt.Errorf("listing pets: %v", err)
}
for _, p := range pets.Payload {
id := p.ID
p.ID = 0
_, err := pu.Pet.PetCreate(ctx, &pet.CreateParams{Pet: p})
if err != nil {
return fmt.Errorf("duplicating pet %d: %v", id, err)
}
}
return nil
}
In the main function, this type could be initiated with the “real” client:
func main() {
var (
cl = client.New(client.Config{})
user = PetUser{Pet: cl.Pet}
ctx = context.Background()
)
err := user.Duplicate(ctx)
if err != nil {
log.Fatal(err)
}
}
On the other hand, in the PetUser
unit tests, we can use the client mock:
func TestPetUser_Duplicate(t *testing.T) {
var (
m pet.MockAPI
pu = PetUser{Pet: &m}
ctx = context.Background()
)
m.On("PetList", ctx, (*pet.PetListParams)(nil)).
Return(&pet.PetListOK{Payload: petList}, nil).
Once()
m.On("PetCreate", ctx, &pet.PetCreateParams{Pet: &models.Pet{Kind: "dog", Name: swag.String("Bonni")}}).
Return(&pet.PetCreateCreated{Payload: petList[0]}, nil).
Once()
m.On("PetCreate", ctx, &pet.PetCreateParams{Pet: &models.Pet{Kind: "cat", Name: swag.String("Mitzi")}}).
Return(&pet.PetCreateCreated{Payload: petList[1]}, nil).
Once()
err := pu.Duplicate(ctx)
assert.Nil(t, err)
m.AssertExpectations(t)
}
Wrap Up
go-swagger is a very powerful framework to develop high scale REST APIs. In stratoscale we found some gaps in integrating go-swagger, using it, and testing it. go-swagger’s flexibility, and the go language flexibility helped us overcome those gaps, and the result is amazing.
We are very grateful to the go-swagger team, and hope go-swagger will continue to develop.