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:

  1. Uses Stratoscale’s container image
  2. Uses a versioned image - to maintain reproducibility of builds.
  3. Removes the unnessecary -it flags (stand for interactive and TTY docker run)
  4. Mounts $(pwd) and not ${HOME} which will mount less data into the container and will work if you are not under ${HOME}.
  5. Adds -u which changes the user in the container, so the generated files won’t be owned by the root 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:

  1. 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.
  2. 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.

Written on July 5, 2018