627

GitHub - deepmap/oapi-codegen: Generate Go client and server boilerplate from Op...

 4 years ago
source link: https://github.com/deepmap/oapi-codegen
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

README.md

OpenAPI Client and Server Code Generator

This package contains a set of utilities for generating Go boilerplate code for services based on OpenAPI 3.0 API definitions. When working with microservices, it's important to have an API contract which servers and clients both imprement to minimize the chances of incompatibilities. It's tedious to generate Go models which precisely correspond to OpenAPI specifications, so let our code generator do that work for you, so that you can focus on implementing the business logic for your service.

We have chosen to use Echo as our HTTP routing engine, due to its speed and simplicity for the generated stubs.

This package tries to be too simple rather than too generic, so we've made some design decisions in favor of simplicity, knowing that we can't generate strongly typed Go code for all possible OpenAPI Schemas.

Overview

We're going to use the OpenAPI example of the Expanded Petstore in the descriptions below, please have a look at it.

In order to create a Go server to serve this exact schema, you would have to write a lot of boilerplate code to perform all the marshalling and unmarshalling into objects which match the OpenAPI 3.0 definition. The code generator in this directory does a lot of that for you. You would run it like so:

go get github.com/deepmap/oapi-codegen/cmd/oapi-codegen
oapi-codegen petstore-expanded.yaml  > petstore.gen.go

Let's go through that petstore.gen.go file to show you everything which was generated.

Generated Server Boilerplate

The /components/schemas section in OpenAPI defines reusable objects, so Go types are generated for these. The Pet Store example defines Error, Pet, Pets and NewPet, so we do the same in Go:

// Type definition for component schema "Error"
type Error struct {
    Code    int32  `json:"code"`
    Message string `json:"message"`
}

// Type definition for component schema "NewPet"
type NewPet struct {
    Name string  `json:"name"`
    Tag  *string `json,omitempty:"tag"`
}

// Type definition for component schema "Pet"
type Pet struct {
    // Embedded struct due to allOf(#/components/schemas/NewPet)
    NewPet
    // Embedded fields due to inline allOf schema
    Id int64 `json:"id"`
}

// Type definition for component schema "Pets"
type Pets []Pet

It's best to define objects under /components field in the schema, since those will be turned into named Go types. If you use inline types in your handler definitions, we will generate inline, anonymous Go types, but those are more tedious to deal with since you will have to redeclare them at every point of use.

For each element in the paths map in OpenAPI, we will generate a Go handler function in an interface object. Here is the generated Go interface for our server.

type ServerInterface interface {
    //  (GET /pets)
    FindPets(ctx echo.Context, params FindPetsParams) error
    //  (POST /pets)
    AddPet(ctx echo.Context) error
    //  (DELETE /pets/{id})
    DeletePet(ctx echo.Context, id int64) error
    //  (GET /pets/{id})
    FindPetById(ctx echo.Context, id int64) error
}

These are the functions which you will implement yourself in order to create a server conforming to the API specification. Normally, all the arguments and parameters are stored on the echo.Context in handlers, so we do the tedious work of of unmarshaling the JSON automatically, simply passing values into your handlers.

Notice that FindPetById takes a parameter id int64. All path arguments will be passed as arguments to your function, since they are mandatory.

Remaining arguments can be passed in headers, query arguments or cookies. Those will be written to a params object. Look at the FindPets function above, it takes as input FindPetsParams, which is defined as follows:

// Parameters object for FindPets
type FindPetsParams struct {
   Tags  *[]string `json:"tags,omitempty"`
   Limit *int32   `json:"limit,omitempty"`
}

The HTTP query parameter limit turns into a Go field named Limit. It is passed by pointer, since it is an optional parameter. If the parameter is specified, the pointer will be non-nil, and you can read its value.

If you changed the OpenAPI specification to make the parameter required, the FindPetsParams structure will contain the type by value:

type FindPetsParams struct {
    Tags  *[]string `json:"tags,omitempty"`
    Limit int32   `json:"limit"`
}

The usage of Echo is out of scope of this doc, but once you have an echo instance, we generate a utility function to help you associate your handlers with this autogenerated code. For the pet store, it looks like this:

func RegisterHandlers(router codegen.EchoRouter, si ServerInterface) {
    wrapper := ServerInterfaceWrapper{
        Handler: si,
    }
    router.GET("/pets", wrapper.FindPets)
    router.POST("/pets", wrapper.AddPet)
    router.DELETE("/pets/:id", wrapper.DeletePet)
    router.GET("/pets/:id", wrapper.FindPetById)
}

The wrapper functions referenced above contain generated code which pulls parameters off the Echo request context, and unmarshals them into Go objects.

You would register the generated handlers as follows:

func SetupHandler() {
    var myApi PetStoreImpl  // This implements the pet store interface
    e := echo.New()
    petstore.RegisterHandlers(e, &myApi)
    ...
}

Generated Client Boilerplate

Once your server is up and running, you probably want to make requests to it. If you're going to do those requests from your Go code, we also generate a client which is conformant with your schema to help in marshaling objects to JSON. It uses the same types and similar function signatures to your request handlers.

Here's what we generate for the petstore:

// A client which conforms to the OpenAPI3 specification for this service. The
// server should be fully qualified with shema and server, ie,
// https://deepmap.com.
type Client struct {
    Server string
    Client http.Client
}

// Request for FindPets
func (c *Client) FindPets(ctx context.Context, params *FindPetsParams) (*http.Response, error) {...}

// Request for AddPet with JSON body
func (c *Client) AddPet(ctx context.Context, body NewPet) (*http.Response, error) {...}

// Request for DeletePet
func (c *Client) DeletePet(ctx context.Context, id int64) (*http.Response, error) {...}

// Request for FindPetById
func (c *Client) FindPetById(ctx context.Context, id int64) (*http.Response, error) {...}

Each operation in your OpenAPI spec will result in a client function which takes the same arguments. It's difficult to handle any arbitrary body that Swagger supports, so we've done some special casing for bodies, and you may get more than one function for an operation with a request body.

  1. If you have more than one request body type, meaning more than one media type, you will have a generic handler of this form:

     AddPet(ctx context.Context, contentType string, body io.Reader)
    
  2. If you have only a JSON request body, you will get:

     AddPet(ctx context.Context, body NewPet)
    
  3. If you have multiple request body types, which include a JSON type you will get two functions. We've chosen to give the JSON version a shorter name, as we work with JSON and don't want to wear out our keyboards.

     AddPet(ctx context.Context, body NewPet)
     AddPetWithBody(ctx context.Context, contentType string, body io.Reader)
    

The Client object above is fairly flexible, since you can pass in your own http.Client, but we can't foresee all possible usages, so those functions call through to a bunch of helper functions which create requests. In the case of the pet store, we have:

// Request generator for FindPets
func NewFindPetsRequest(server string, params *FindPetsParams) (*http.Request, error) {...}

// Request generator for AddPet with JSON body
func NewAddPetRequest(server string, body NewPet) (*http.Request, error) {...}

// Request generator for AddPet with non-JSON body
func NewAddPetRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) {...}

// Request generator for DeletePet
func NewDeletePetRequest(server string, id int64) (*http.Request, error) {...}

// Request generator for FindPetById
func NewFindPetByIdRequest(server string, id int64) (*http.Request, error) {...}

You can call these functions to build an http.Request from Go objects, which will correspond to your request schema. They map one-to-one to the functions on the client, except that we always generate the generic non-JSON body handler.

There are some caveats to using this code.

  • exploded, form style query arguments, which are the default argument format in OpenAPI 3.0 are undecidable. Say that I have two objects, one composed of the fields (name=bob, id=5) and another which has (name=shoe, color=brown). The first parameter is named person and the second is named item. The default marshaling style for query args would result in /path/?name=bob,id=5&name=shoe,color=brown. In order to tell what belongs to which object, we'd have to look at all the parameters and try to deduce it, but we're lazy, so we didn't. Don't use exploded form style arguments if you're passing around objects which have similar field names. If you used unexploded form parameters, you'd have /path/?person=name,bob,id,5&item=name,shoe,color,brown, which an be parsed unambiguously.

  • Parameters can be defined via schema or via content. Use the content form for anything other than trivial objects, they can marshal to arbitrary JSON structures. When you send them as cookie (in: cookie) arguments, we will URL encode them, since JSON delimiters aren't allowed in cookies.

What's missing

This code is still young, and not complete, since we're filling it in as we need it. We've not yet implemented several things:

  • oneOf, anyOf are not supported with strong Go typing. This schema:

      schema:
        oneOf:
          - $ref: '#/components/schemas/Cat'
          - $ref: '#/components/schemas/Dog'
    

    will result in a Go type of interface{}. It will be up to you to validate whether it conforms to Cat and/or Dog, depending on the keyword. It's not clear if we can do anything much better here given the limits of Go typing.

    allOf is supported, by taking the union of all the fields in all the component schemas. This is the most useful of these operations, and is commonly used to merge objects with an identifier, as in the petstore-expanded example.

  • additionalProperties isn't supported, and will exit with an error. This should be possible to support in the future via a map[string]interface{} or map[string]string.

  • patternProperties isn't yet supported and will exit with an error. This too should be possible to implement.

Making changes to code generation

The code generator uses a tool to inline all the template definitions into code, so that we don't have to deal with the location of the template files. When you update any of the files under the templates/ directory, you will need to regenerate the template inlines:

templates -s pkg/codegen/templates/ > pkg/codegen/templates/templates.gen.go

All this command does is inline the files ending in .tmpl into the specified Go file. This command is found here:

go get github.com/cyberdelia/templates

You can also run go generate, since we've set up those hooks.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK