OpenAPI Client and Server Code Generator

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 services, it's important to have an API contract which servers and clients both implement 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 default HTTP routing engine, due to its speed and simplicity for the generated stubs, and Chi is also supported as an alternative.

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.

This repository is a hard fork of deepmap/oapi-codegen. This new version plans to diverge from the original repository with different design goals and more emphasis on go-chi.

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 install github.com/discord-gophers/goapi-gen/cmd/goapi-gen@latest
goapi-gen 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:"tag,omitempty"`
}

// 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 Echo 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"`
}

Registering handlers

There are a few ways of registering your http handler based on the type of server generated i.e. -generate server or -generate chi-server

Echo

Code generated using -generate server.

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)
    ...
}
Chi

Code generated using -generate chi-server.

type PetStoreImpl struct {}
func (*PetStoreImpl) GetPets(w http.ResponseWriter, r *http.Request) {
    // Implement me
}

func SetupHandler() {
    var myApi PetStoreImpl

    r := chi.NewRouter()
    r.Mount("/", Handler(&myApi))
}
net/http

Chi is 100% compatible with net/http allowing the following with code generated using -generate chi-server.

type PetStoreImpl struct {}
func (*PetStoreImpl) GetPets(w http.ResponseWriter, r *http.Request) {
    // Implement me
}

func SetupHandler() {
    var myApi PetStoreImpl

    http.Handle("/", Handler(&myApi))
}

Additional Properties in type definitions

OpenAPI Schemas implicitly accept additionalProperties, meaning that any fields provided, but not explicitly defined via properties on the schema are accepted as input, and propagated. When unspecified, the additionalProperties field is assumed to be true.

Additional properties are tricky to support in Go with typing, and require lots of boilerplate code, so in this library, we assume that additionalProperties defaults to false and we don't generate this boilerplate. If you would like an object to accept additionalProperties, specify a schema for additionalProperties.

Say we declared NewPet above like so:

    NewPet:
      required:
        - name
      properties:
        name:
          type: string
        tag:
          type: string
      additionalProperties:
        type: string

The Go code for NewPet would now look like this:

// NewPet defines model for NewPet.
type NewPet struct {
	Name                 string            `json:"name"`
	Tag                  *string           `json:"tag,omitempty"`
	AdditionalProperties map[string]string `json:"-"`
}

The additionalProperties, of type string become map[string]string, which maps field names to instances of the additionalProperties schema.

// Getter for additional properties for NewPet. Returns the specified
// element and whether it was found
func (a NewPet) Get(fieldName string) (value string, found bool) {...}

// Setter for additional properties for NewPet
func (a *NewPet) Set(fieldName string, value string) {...}

// Override default JSON handling for NewPet to handle additionalProperties
func (a *NewPet) UnmarshalJSON(b []byte) error {...}

// Override default JSON handling for NewPet to handle additionalProperties
func (a NewPet) MarshalJSON() ([]byte, error) {...}w

There are many special cases for additionalProperties, such as having to define types for inner fields which themselves support additionalProperties, and all of them are tested via the internal/test/components schemas and tests. Please look through those tests for more usage examples.

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.

The interface for the pet store looks like this:

// The interface specification for the client above.
type ClientInterface interface {

	// FindPets request
	FindPets(ctx context.Context, params *FindPetsParams, reqEditors ...RequestEditorFn) (*http.Response, error)

	// AddPet request with JSON body
	AddPet(ctx context.Context, body NewPet, reqEditors ...RequestEditorFn) (*http.Response, error)

	// DeletePet request
	DeletePet(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error)

	// FindPetById request
	FindPetById(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error)
}

A Client object which implements the above interface is also generated:

// Client which conforms to the OpenAPI3 specification for this service.
type Client struct {
    // The endpoint of the server conforming to this interface, with scheme,
    // https://api.example.com for example.
    Server string

    // HTTP client with any customized settings, such as certificate chains.
    Client http.Client

    // A callback for modifying requests which are generated before sending over
    // the network.
    RequestEditors []func(ctx context.Context, req *http.Request) 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 and a request editing callback. You can use that callback to add headers. In our middleware stack, we annotate the context with additional information such as the request ID and function tracing information, and we use the callback to propagate that information into the request headers. Still, we can't foresee all possible usages, so those functions call through to 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.

Using SecurityProviders

If you generate client-code, you can use some default-provided security providers which help you to use the various OpenAPI 3 Authentication mechanism.

    import (
        "github.com/discord-gophers/goapi-gen/pkg/securityprovider"
    )

    func CreateSampleProviders() error {
        // Example BasicAuth
        // See: https://swagger.io/docs/specification/authentication/basic-authentication/
        basicAuthProvider, basicAuthProviderErr := securityprovider.NewSecurityProviderBasicAuth("MY_USER", "MY_PASS")
        if basicAuthProviderErr != nil {
            panic(basicAuthProviderErr)
        }

        // Example BearerToken
        // See: https://swagger.io/docs/specification/authentication/bearer-authentication/
        bearerTokenProvider, bearerTokenProviderErr := securityprovider.NewSecurityProviderBearerToken("MY_TOKEN")
        if bearerTokenProviderErr != nil {
            panic(bearerTokenProviderErr)
        }

        // Example ApiKey provider
        // See: https://swagger.io/docs/specification/authentication/api-keys/
        apiKeyProvider, apiKeyProviderErr := securityprovider.NewSecurityProviderApiKey("query", "myApiKeyParam", "MY_API_KEY")
        if apiKeyProviderErr != nil {
            panic(apiKeyProviderErr)
        }

        // Example providing your own provider using an anonymous function wrapping in the
        // InterceptoFn adapter. The behaviour between the InterceptorFn and the Interceptor interface
        // are the same as http.HandlerFunc and http.Handler.
        customProvider := func(req *http.Request, ctx context.Context) error {
            // Just log the request header, nothing else.
            log.Println(req.Header)
            return nil
        }

        // Exhaustive list of some defaults you can use to initialize a Client.
        // If you need to override the underlying httpClient, you can use the option
        //
        // WithHTTPClient(httpClient *http.Client)
        //
        client, clientErr := NewClient("https://api.deepmap.com", WithRequestEditorFn(apiKeyProvider.Intercept))

        return nil
    }

Extensions

goapi-gen supports the following extended properties:

  • x-go-type: specifies Go type name. It allows you to specify the type name for a schema, and will override any default value. This extended property isn't supported in all parts of OpenAPI, so please refer to the spec as to where it's allowed. Swagger validation tools will flag incorrect usage of this property.

  • x-goapi-gen-extra-tags: adds extra Go field tags to the generated struct field. This is useful for interfacing with tag based ORM or validation libraries. The extra tags that are added are in addition to the regular json tags that are generated. If you specify your own json tag, you will override the default one.

    components:
      schemas:
        Object:
          properties:
            name:
              type: string
              x-goapi-gen-extra-tags:
                tag1: value1
                tag2: value2

    In the example above, field name will be declared as:

    Name string `json:"name" tag1:"value1" tag2:"value2"`
  • x-goapi-gen-middlewares: specifies a list of tagged middlewares. These can be specific middlewares that are operation-specific, as well as path-specific. This is very useful when you want to give a specific routes middleware, but not to all operations. The middleware are always called in the order of definition. If the tagged middleware is not defined, it will be silently skipped.

    /pets:
      x-goapi-gen-middlewares: [validateJSON]
      get:
        x-goapi-gen-middlewares: [limit]

    In the example above, the following middleware calls will be added to your handler:

    // Operation specific middleware
    if siw.TaggedMiddlewares != nil {
      if middleware, ok := siw.TaggedMiddlewares["validateJSON"]; ok {
        handler = middleware(handler)
      }
      if middleware, ok := siw.TaggedMiddlewares["limit"]; ok {
        handler = middleware(handler)
      }
    }

Using goapi-gen

The default options for goapi-gen will generate everything; client, server, type definitions and embedded swagger spec, but you can generate subsets of those via the -generate flag. It defaults to types,client,server,spec, but you can specify any combination of those.

  • types: generate all type definitions for all types in the OpenAPI spec. This will be everything under #components, as well as request parameter, request body, and response type objects.
  • server: generate the Echo server boilerplate. server requires the types in the same package to compile.
  • chi-server: generate the Chi server boilerplate. This code is dependent on that produced by the types target.
  • client: generate the client boilerplate. It, too, requires the types to be present in its package.
  • spec: embed the OpenAPI spec into the generated code as a gzipped blob. This
  • skip-fmt: skip running goimports on the generated code. This is useful for debugging the generated file in case the spec contains weird strings.
  • skip-prune: skip pruning unused components from the spec prior to generating the code.
  • import-mapping: specifies a map of references external OpenAPI specs to go Go include paths. Please see below.

So, for example, if you would like to produce only the server code, you could run goapi-gen -generate types,server. You could generate types and server into separate files, but both are required for the server code.

goapi-gen can filter paths base on their tags in the openapi definition. Use either -include-tags or -exclude-tags followed by a comma-separated list of tags. For instance, to generate a server that serves all paths except those tagged with auth or admin, use the argument, -exclude-tags="auth,admin". To generate a server that only handles admin paths, use the argument -include-tags="admin". When neither of these arguments is present, all paths are generated.

goapi-gen can filter schemas based on the option --exclude-schemas, which is a comma separated list of schema names. For instance, --exclude-schemas=Pet,NewPet will exclude from generation schemas Pet and NewPet. This allow to have a in the same package a manually defined structure or interface and refer to it in the openapi spec.

Since go generate commands must be a single line, all the options above can make them pretty unwieldy, so you can specify all of the options in a configuration file via the --config option. Please see the test under /internal/test/externalref/ for an example. The structure of the file is as follows:

output:
  externalref.gen.go
package: externalref
generate:
  - types
  - skip-prune
import-mapping:
  ./packageA/spec.yaml: github.com/discord-gophers/goapi-gen/internal/test/externalref/packageA
  ./packageB/spec.yaml: github.com/discord-gophers/goapi-gen/internal/test/externalref/packageB

Have a look at cmd/goapi-gen/goapi-gen.go to see all the fields on the configuration structure.

Import Mappings

OpenAPI specifications may contain references to other OpenAPI specifications, and we need some additional information in order to be able to generate correct Go code.

An external reference looks like this:

$ref: ./some_spec.yaml#/components/schemas/Type

We assume that you have already generated the boilerplate code for ./some_spec.yaml using goapi-gen, and you have a package which contains the generated code, let's call it github.com/discord-gophers/some-package. You need to tell goapi-gen that some_spec.yaml corresponds to this package, and you would do it by specifying this command line argument:

-import-mapping=./some_spec.yaml:github.com/discord-gophers/some-package

This tells us that in order to resolve references generated from some_spec.yaml we need to import github.com/discord-gophers/some-package. You may specify multiple mappings by comma separating them in the form key1:value1,key2:value2.

What's missing or incomplete

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.

  • patternProperties isn't yet supported and will exit with an error. Pattern properties were defined in JSONSchema, and the kin-openapi Swagger object knows how to parse them, but they're not part of OpenAPI 3.0, so we've left them out, as support is very complicated.

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:

go generate ./pkg/codegen/templates

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

Afterwards you should run go generate ./..., and the templates will be updated accordingly.

Alternatively, you can provide custom templates to override built-in ones using the -templates flag specifying a path to a directory containing templates files. These files must be named identically to built-in template files (see pkg/codegen/templates/*.tmpl in the source code), and will be interpreted on-the-fly at run time. Example:

$ ls -1 my-templates/
client.tmpl
typedef.tmpl
$ goapi-gen \
    -templates my-templates/ \
    -generate types,client \
    petstore-expanded.yaml
Owner
Discord Gophers
This is a space for the members of the Discord Gophers chat server to collaborate on projects, hackathons, and what not.
Discord Gophers
Comments
  • Middleware Ordering

    Middleware Ordering

    We really need documentation on how middleware is processed, as I now found out. The last middleware is the one which is called first, so:

    x-go-middlewares: [auth,something-that-needs-auth]
    

    will run something-that-needs-auth first. Ordering is intact, however, you would really think it is the other way around.

  • proposal: add extension x-go-type-external for external type references

    proposal: add extension x-go-type-external for external type references

    I needed something to be able to set the go types to custom made types. x-go-type allows for this but currently only for types which are defined in the same scope as the generated code. As this is highly impractical for me, I introduced x-go-type-external.

    I did not rewrite the x-go-type as a) I really didn't want to destroy that part ๐Ÿ˜„ b) I think it makes sense to have those two options seperated as one is generally for scope level and one gives you all the options you need, but needs more options filled out.

    How does it work?

    You can use it the same way as x-go-type and it overwrites the given standard type:

    x-go-type-external:
        type: CustomString
        import: github.com/discord-gophers/goapi-gen/examples/petstore-expanded/types
        alias: customTypes
    

    This gives you the option to reference the custom type, here CustomString, and specify the import path. The alias option is available so we can make sure not to make go angry because we import time and github.com/tiehm/time for example but assign it an alias. alias is currently mandatory but could be made optional as you might not need it for every external type.

    The resultung code uses the alias and type to generate the typing, for example:

    type NewPet struct {
    	// Name of the pet
            //         vvv alias       vvv type          
    	Name customTypes.CustomString `json:"name"`
    
    	// Type of the pet
    	Tag *string `json:"tag,omitempty"`
    }
    

    The working example is also attached in the petstore-extended example.

    Please forgive me if this is not the cleanest code or if I made spelling mistakes in the PR... it is 5:30 am but I really wanted to try this out.

    I would love some feedback on it and of course suggestions on how to make this better, as I am sure my approach is not the best possible.

  • Feature Idea: Add automatic decoding of request bodies

    Feature Idea: Add automatic decoding of request bodies

    Right now you have to manually decode every request body on every request handler which uses request bodies. It's almost always pretty much the same stuff like

    var newPet NewPet
    if err := json.NewDecoder(r.Body).Decode(&newPet); err != nil {
    	sendPetstoreError(w, http.StatusBadRequest, "Invalid format for NewPet")
    	return
    }
    

    This could be circumvented by introducing a feature to automatically generate/decode the bodies in the generated request handler before it is passed to the handler which is written by the developer.

    As an example from the petstore example: We switch from

    // ServerInterface represents all server handlers.
    type ServerInterface interface {
    	// ...
    	// Creates a new pet
    	// (POST /pets)
    	AddPet(w http.ResponseWriter, r *http.Request)
    	// ...
    }
    

    to

    // ServerInterface represents all server handlers.
    type ServerInterface interface {
    	// ...
    	// Creates a new pet
    	// (POST /pets)
    	AddPet(w http.ResponseWriter, r *http.Request, body NewPet)
    	// ...
    }
    

    and directly pass the body the handler which then can implement it and directly use it:

    func (p *PetStore) AddPet(w http.ResponseWriter, r *http.Request, body NewPet) {
    	fmt.Sprintf("name is %s", body.Name)
    }
    

    instead of

    func (p *PetStore) AddPet(w http.ResponseWriter, r *http.Request) {
    	var body NewPet
    	if err := json.NewDecoder(r.Body).Decode(&newPet); err != nil {
    		sendPetstoreError(w, http.StatusBadRequest, "Invalid format for NewPet")
    		return
    	}
    	fmt.Sprintf("name is %s", body.Name)
    }
    

    This could also implement required checks on body fields and error if those fail automatically without checking it everytime you decode the request body.

    The issue behind this is obviously that you now have less control over your error handling for this specific case. A solution could be a global optional function which can be changed to spit out dynamic errors if needed. An example could be:

    // maybe a bit too long of a function name :)
    func dynamicDecodeErrorsOnRequiredFieldFails(route, field string, err error) string {
        return fmt.Sprintf("invalid body: field %s has issue %s", field, err.Error())
    }
    

    or anything like that. This way custom errors could be kept while letting the codegen take care of decoding the body. Another option could be to not pass the body structure itself to the handler but a struct which has the body in it (if it was successfully decoded) and the error if there is any. This way you have full control over everything but this of course adds a bit more code and changes types.

  • Add goreleaser

    Add goreleaser

    As title. This will require a secret called GITHUB_TOKEN, however.

    Not sure who wants to generate one or if we want a machine user for discord-gophers,

  • Proposal: ResponseCode

    Proposal: ResponseCode

    Context

    For methods such as DELETE, PATCH, PUT and POST, we are often met with the need of just returning a response code.

    Having a helper method landing after #80 (returning a *Response) directly means that we are obliged to either return something like

    (we will go with openapi as the generated package name)

    var r openapi.Response
    r.Status(http.StatusNoContent)
    return &r
    

    Or to write the status code directly, similarly to this code @diamondburned refactored.

    w.WriteHeader(http.StatusNoContent)
    return nil
    

    Proposal

    Generating a function to help with this

    func ResponseCode(code int) *Response {
    	return &Response{
    		statusCode: code,
    	}
    }
    

    Which would turn this situation into

    return openapi.ResponseCode(http.StatusNoContent)
    

    I think this would make every single return meaningful, such that developers could identify code paths that actually respond to the request simpler, rather than having them search for status code being set, or not, earlier.

  • fix(codegen): required param error implementation

    fix(codegen): required param error implementation

    • return correctly required param
    • delete dead code
    • replace usage of deprecated function
    • update go:generate command of moq to use latest, since it seems to be causing issues.
    • update required param error tests

    POSSIBLE BREAKING: multiple word call to title helper function (no change in generation, and it generates valid code, but to be aware if needs be).

  • Unmarshaling string to enum produces empty value

    Unmarshaling string to enum produces empty value

    Unmarshalling a string to an enum currently unmarshals an empty string because the value is never set and persisted internally.

    This MR fixes two issues:

    • UnmarshalJSON never updated the internal value after unmarshaling.
    • Methods must have a pointer receiver for updates to take effect.
  • Convert enums to be struct-based

    Convert enums to be struct-based

    Currently, enums are converted into const values of a particular type, such as:

    type WebhookEvent string
    
    const (
    	WebhookEventShippingUpdated WebhookEvent = "ShippingUpdated"
    )
    

    This is suboptimal since any parameter that accepts a WebhookEvent would still accept any arbitrary string.

    A good workaround would be to use struct-based enums, allowing for a greater measure of type safety, generating code similar to:

    type WebhookEvent struct {
    	value string
    }
    
    func (e WebhookEvent) String() string {
    	return e.value
    }
    
    var (
    	Unknown   = WebhookEvent{""}
    	ShippingUpdated = WebhookEvent{"ShippingUpdated"}
    )
    
  • Generate file types from flags

    Generate file types from flags

    Why

    Having a few thousand lines of generated code in a single file isn't always readable. Having the ability to break down those files for easier navigation is something I was using before, but had to run 2 commands.

    With the revamp of the cli, I am hoping that this can be natively supported.

    Example

    This following snippet should generate 2 files, a types one, with all the structs and possibly Binder methods, and a server one, with the routing, binding and interfaces.

    Example command

    goapi-gen --server=server.gen.go --types=types.gen.go
    

    Edge case

    By default, the server relies on the models, so in case the typed flag is empty, the types should be generated too, in the server.

  • Update full package repository structure

    Update full package repository structure

    The current repository has a lot of redundant directories and has no Go code in the project root. This can most likely be updated to be more easy to work with, as well as easier to go install.

  • Drop Echo support

    Drop Echo support

    In a first step to making this hard-fork simpler to maintain and better to use for members of discord gophers, dropping Echo support due to the lack of need and additional complexity is a must.

    Tasks:

    • [ ] Edit CLI
    • [ ] Edit README
    • [ ] Remove Templates
    • [ ] Remove Middleware code
  • Generate middlewares as fields

    Generate middlewares as fields

    This PR cleans up the ugly middleware map in favor of a clean, generated struct.

    The goal is to profit from the excellent autocompletion provided by IDEs to work faster.

    As an added bonus, if you had previously used a middleware and forgot it in the spec, the code wont compile, as opposed to silently running, ensuring not ever forgetting a middleware both in the code and in the spec (If you've never typed it, I can't do anything for you)

  • make handler return type interfaces

    make handler return type interfaces

    This PR adds a Reponser interface, which enables us to construct any form of custom type and turn them into responses accepted by goapi-gen.

    The goal of this proposal is to shorten the error handling code, and clean up ugly multi-line errors made when constructing a non-standard/specified response body.

    The example provided with the petstore may not be the best because of it's short nature, but we can easily save dozens of lines of code, and standardize better with such an API.

  • [PROPOSAL] returning interface from handlers

    [PROPOSAL] returning interface from handlers

    Right now, returning concrete types is nice for well defined return types, such as 200s, but once you want to return simple errors, it becomes a pain.

    For example, let's say I want to return an InternalServerError with an error code and clarification.

    With the status quo, this looks like:

    	return CreateThingJSONDefaultResponse(
    		DefaultResponse{Code: "spiky-sun-turtle", Message: http.StatusText(http.StatusInternalServerError)},
    	).Status(500)
    

    This gets really verbose really fast. What we could have instead is

    type InternalServerError struct {
        Code    string `json:"code"`
        Message string `json:"message"`
    }
    
    func (e InternalServerError) Respond() *Response {
        return Response{Code: 500, Body: e, ContentType: "application/json"}
    }
    

    and use it like so:

    return InternalServerError{Code: "goop-turtle", Message: http.StatusText(http.StatusInternalServerError)}
    

    Any type implementing

    type Responder interface {
         Respond() *Response
    }
    

    Could be returned, thus easily implementing custom errors in a much less verbose way.

    This would also allow us to have possible hooks in responses, to avoid having more telemetry/logging directly in the handler, since those types could only wrap parts intended for the users, while we could abstract the telemetry to our own functions, thus making error handling go from 5/7 lines per error to 1 or 2.

  • Support

    Support "in: formData" parameters

    goapi-gen currently errors out with an "in": "formData" parameter:

            "consumes": [
              "multipart/form-data"
            ],
            "parameters": [
              {
                "in": "formData",
                "name": "content",
                "type": "file",
                "description": "The content of the file.",
                "required": true
              }
            ],
    

    yields the error

    error: could not generate code: error creating operation definitions: error describing global parameters for POST//assets: error generating type for param (content): parameter 'content' has no schema or content
    
  • Proposal: Autogenerate Security Scheme checkers

    Proposal: Autogenerate Security Scheme checkers

    Problem

    goapi-gen currently does not autogenerate anything for security. Instead, the user is supposed to use a long-winded way of adding a middleware which also doesn't support dependency injection via context.Contexts.

    The current method also requires the user to manually verify authorization information by reading the *http.Request instance within the authentication callback.

    Below is an example of such code for BearerAuth:

    func (h handler) authenticate(ctx context.Context, in *openapi3filter.AuthenticationInput) error {
    	switch in.SecuritySchemeName {
    	case "BearerAuth":
    		auth := in.RequestValidationInput.Request.Header.Get("Authorization")
    		if !strings.HasPrefix(auth, "Bearer ") {
    			return fmt.Errorf("invalid BearerAuth header: missing prefix")
    		}
    
    		auth = strings.TrimPrefix(auth, "Bearer ")
    
    		s, err := h.server.AuthorizerServer().Authorize(ctx, auth)
    		if err != nil {
    			return err
    		}
    
    		// Checking is done, but s cannot be passed down!
    		return nil
    	default:
    		return fmt.Errorf("unsupported auth scheme %q", in.SecuritySchemeName)
    	}
    }
    

    This code is then used like so:

    	validator := middleware.OapiRequestValidatorWithOptions(nil, &middleware.Options{
    		Options: openapi3filter.Options{
    			AuthenticationFunc: handler.authenticate,
    		},
    	})
    

    This abstraction by itself isn't any easier than just using a regular middleware, except for the fact that it's still not possible to inject any value into later handlers. For comparison, such a middleware would look something like this:

    func (h handler) mustAuthorize(next http.Handler) http.Handler {
    	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		token := r.Header.Get("Authorization")
    		if !strings.HasPrefix(token, "Bearer ") {
    			h.writeError(w, 401, fmt.Errorf("invalid BearerAuth header: missing prefix"))
    			return
    		}
    
    		token = strings.TrimPrefix(token, "Bearer ")
    
    		s, err := h.server.AuthorizerServer().Authorize(r.Context(), token)
    		if err != nil {
    			h.writeError(w, 401, err)
    			return
    		}
    
    		ctx := context.WithValue(r.Context(), sessionKey, s)
    		next.ServeHTTP(w, r.WithContext(ctx))
    	})
    }
    

    Clearly, goapi-gen isn't generating much at all when it comes to security.


    Proposal

    A possible way to improve this situation would be to make goapi-gen generate middlewares that inject the related security information for the user to check from a middleware.

    For example, given the following JSON (with all routes and schemas omitted):

    {
      "security": [
        {
          "BearerAuth": [],
        }
      ],  
      "components": {
        "securitySchemes": {
          "BearerAuth": {
            "type": "http",
            "scheme": "bearer"
          }
        }
      }
    }
    

    goapi-gen can generate code that handles checking for a valid security scheme and validating the Authorization: Bearer header. It can then generate the above callbacks, which can be tucked inside the autogenerated ServerOptions.

    For the above example, that would look like this:

    // UnauthorizedError exists so the user can
    type UnauthorizedError struct {
    	error
    	SecuritySchemer SecuritySchemer
    }
    
    type ctxKey uint8
    
    const (
    	_ ctxKey = iota
    	securityCtxKey
    )
    
    // SecuritySchemer describes any of the security methods.
    type SecuritySchemer interface {
    	SecurityScheme() *openapi3.SecurityScheme
    	SecuritySchemeName() string
    }
    
    // SecuritySchemeFromContext returns a SecuritySchemer for the current endpoint,
    // if any. The user should type-switch on the returned result.
    func SecuritySchemeFromContext(ctx context.Context) SecuritySchemer {
    	ss, _ := ctx.Value(securityCtxKey).(SecuritySchemer)
    	return ss
    }
    
    // BearerAuthScheme is a SecuritySchemer type for BearerAuth.
    type BearerAuthScheme struct {
    	// Bearer contains the token from the Authorization header.
    	Bearer string
    }
    
    // I'm not sure what goes in here.
    func (s *BearerAuthScheme) SecurityScheme() *openapi3.SecurityScheme { return nil }
    
    // SecuritySchemeName implements SecuritySchemer.
    func (s *BearerAuthScheme) SecuritySchemeName() string { "BearerAuth" }
    
    func handleBearerAuth(next http.Handler) http.Handler {
    	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		scheme := &BearerAuthScheme{}
    
    		auth := r.Header.Get("Authorization")
    		if !strings.HasPrefix(auth, "Bearer ") {
    			err := fmt.Errorf("invalid format for Authorization header: missing prefix")
    			siw.ErrorHandlerFunc(w, r, &UnauthorizedError{err, scheme})
    			return
    		}
    
    		auth = strings.TrimPrefix(auth, "Bearer")
    
    		ctx = context.WithValue(ctx, securityCtxKey, scheme)
    		next.ServeHTTP(w, r.WithContext(ctx))
    	})
    }
    
    type ServerOptions struct {
    	BaseURL            string
    	BaseRouter         chi.Router
    	Middlewares        map[string]func(http.Handler) http.Handler
    	SecurityMiddleware func(next http.Handler) http.Handler
    	ErrorHandlerFunc   func(w http.ResponseWriter, r *http.Request, err error)
    }
    
    // Handler creates http.Handler with routing matching OpenAPI spec.
    func Handler(si ServerInterface, opts ...ServerOption) http.Handler {
    	options := &ServerOptions{
    		BaseURL:     "/",
    		BaseRouter:  chi.NewRouter(),
    		Middlewares: make(map[string]func(http.Handler) http.Handler),
    		ErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) {
    			http.Error(w, err.Error(), http.StatusBadRequest)
    		},
    	}
    
    	for _, f := range opts {
    		f(options)
    	}
    
    	r := options.BaseRouter
    	wrapper := ServerInterfaceWrapper{
    		Handler:          si,
    		Middlewares:      options.Middlewares,
    		ErrorHandlerFunc: options.ErrorHandlerFunc,
    	}
    
    	r.Route(options.BaseURL, func(r chi.Router) {
    		r.Post("/login", wrapper.Login)
    		r.Post("/register", wrapper.Register)
    
    		r.Group(func(r chi.Router) {
    			r.Use(handleBearerAuth)
    			r.Use(options.SecurityMiddleware)
    
    			r.Get("/posts", wrapper.NextPosts)
    			r.Get("/posts/liked", wrapper.GetLikedPosts)
    			r.Delete("/posts/{id}", wrapper.DeletePosts)
    			r.Get("/users/self", wrapper.GetSelf)
    			r.Get("/users/{id}", wrapper.GetUser)
    		})
    	})
    
    	return r
    }
    

    The user can then inject the middleware like so:

    openapi.WithSecurityMiddleware(func(next http.Handler) http.Handler {
    	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		ctx := r.Context()
    
    		switch ss := openapi.SecuritySchemeFromContext(ctx).(type) {
    		case *openapi.BearerAuthScheme:
    			s, err := h.server.AuthorizerServer().Authorize(ctx, ss.Bearer)
    			if err != nil {
    				// TODO: consider a better and more expressive way.
    				h.writeError(w, 401, err)
    				return
    			}
    
    			ctx = context.WithValue(ctx, sessionKey, s)
    		}
    
    		next.ServeHTTP(w, r.WithContext(ctx))
    	})
    })
    

    A few important notes:

    • There should be a better way of writing errors from middlewares than the user calling their own writeError callback.
    • r.Group is required here because the user's SecurityMiddleware must be called after our security middleware handlers (handleBearerAuth in this case) are called. Because of this, the user cannot arbitrarily add a middleware.
    • Not filling out SecurityMiddleware is a runtime error, not a compile-time error.
    • The user must type-assert the error in their ErrorHandlerFunc for the server to possibly respond with a correct status code.
      • This can possibly be improved by making handleBearerAuth not call ErrorHandlerFunc and instead write its own code, like so:
    func handleBearerAuth(next http.Handler) http.Handler {
    	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		scheme := &BearerAuthScheme{}
    
    		auth := r.Header.Get("Authorization")
    		if !strings.HasPrefix(auth, "Bearer ") {
    			http.Error(w, "invalid format for Authorization header: missing prefix", 401)
    			return
    		}
    
    		auth = strings.TrimPrefix(auth, "Bearer")
    
    		ctx = context.WithValue(ctx, securityCtxKey, scheme)
    		next.ServeHTTP(w, r.WithContext(ctx))
    	})
    }
    

    A very important note regarding the above snippet: there is no way for the user to change how the error is written if they feed the backend an invalid header. That is, unfortunately, on par with what OapiRequestValidatorWithOptions is currently doing, which is a terrible idea!

    func OapiRequestValidatorWithOptions(swagger *openapi3.T, options *Options) func(next http.Handler) http.Handler {
    	router, err := gorillamux.NewRouter(swagger)
    	if err != nil {
    		panic(err)
    	}
    
    	return func(next http.Handler) http.Handler {
    		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    
    			// validate request
    			if statusCode, err := validateRequest(r, router, options); err != nil {
    				http.Error(w, err.Error(), statusCode) // !!!
    				return
    			}
    
    			// serve
    			next.ServeHTTP(w, r)
    		})
    	}
    
    }
    

    Alternatives

    I've considered alternatives to injecting into context.Context, including having a callback that the user implements specifically for authenticating. Something like this:

    type SecurityHandlers struct {
    	BearerAuth func(next http.Handler) http.Handler
    	BearerAuth func(ctx context.Context, token string) (context.Context, error)
    }
    

    This API, while slightly more declarative (in that it doesn't require pulling an instance from the blackbox that is context.Context), it has other annoying flaws: the first BearerAuth does no parsing at all, while the second BearerAuth has a generally very weird method signature.

A diff tool for OpenAPI Specification 3

OpenAPI Diff This is a diff tool for OpenAPI Spec 3. It can be used in two ways: Generate a diff report in YAML, Text/Markdown or HTML from the cmd-li

Jan 5, 2023
UPBit Auto Trading with OpenAPI Golang Module

Go-Bit! UPBit Auto Trading System with OpenAPI ์ด ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋Š” upbit๋ฅผ ์œ„ํ•œ ์ž๋™๋งค๋งค ํ”„๋กœ๊ทธ๋žจ์„ ๊ฐœ๋ฐœํ•˜๊ธฐ ์œ„ํ•ด ์ œ๊ณตํ•˜๋Š” go module์ž…๋‹ˆ๋‹ค. Features ๊ตฌํ˜„ ์ž‘์—… ์ง„ํ–‰์ƒํ™ฉ Sample Code Template shiel

Jun 27, 2022
Composable OpenAPI Specification (aka Swagger)
Composable OpenAPI Specification (aka Swagger)

compoas Library for building, composing and serving OpenAPI Specification (aka Swagger). Features This lib provides: golang structs which reflect Open

Jun 1, 2022
Entgo openapi example for go

entgo-openapi-example Example app created with ent entgen swagger-editor See https://entgo.io/blog/2021/09/10/openapi-generator/ License MIT Author Ya

Nov 27, 2021
This project extends the go-chi router to support OpenAPI 3, bringing to you a simple interface to build a router conforming your API contract.

Go OpenAPI This project extends the go-chi router to support OpenAPI 3, bringing to you a simple interface to build a router conforming your API contr

Mar 27, 2022
Conversion from OpenAPI definitions to krakend configuration.

Description Basic conversion from OpenAPI specification to Krakend config. This is extendable with custom OpenAPI attributes and more support for both

Dec 13, 2022
A code generator that turns plain old Go services into RPC-enabled (micro)services with robust HTTP APIs.

Frodo is a code generator and runtime library that helps you write RPC-enabled (micro) services and APIs.

Dec 16, 2022
Generates Golang client and server based on OpenAPI2 (swagger) definitions
Generates Golang client and server based on OpenAPI2 (swagger) definitions

ExperienceOne Golang APIKit ExperienceOne Golang APIKit Overview Requirements Installation Usage Generate standard project structure Define the API wi

Aug 9, 2022
gRPC to JSON proxy generator following the gRPC HTTP spec
gRPC to JSON proxy generator following the gRPC HTTP spec

The gRPC-Gateway is a plugin of the Google protocol buffers compiler protoc. It reads protobuf service definitions and generates a reverse-proxy server which translates a RESTful HTTP API into gRPC. This server is generated according to the google.api.http annotations in your service definitions.

Jan 3, 2023
Social previews generator as a microservice.
Social previews generator as a microservice.

ogimgd Social previews generator as a microservice. Can be used to generate images for og:image meta-tag. It runs as an HTTP server with a single endp

Sep 17, 2022
Proof-of-concept SLSA provenance generator for GitHub Actions

SLSA GitHub Actions Demo A proof-of-concept SLSA provenance generator for GitHub Actions. Background SLSA is a framework intended to codify and promot

Nov 4, 2022
Just a quick demo of how you can use automatically generated protobuffer and gRPC code from buf.build

buf.build demo The purpose of this repository is to demonstrate how to use the services offered by buf.build for hosting protobuffer definitions and a

Jan 4, 2022
Microservice Boilerplate for Golang with gRPC and RESTful API. Multiple database and client supported
Microservice Boilerplate for Golang with gRPC and RESTful API. Multiple database and client supported

Go Microservice Starter A boilerplate for flexible Go microservice. Table of contents Features Installation Todo List Folder Structures Features: Mult

Jul 28, 2022
Starter code for writing web services in Go

Ultimate Service Copyright 2018, 2019, 2020, 2021, Ardan Labs [email protected] Ultimate Service 2.0 Video If you are watching the Ultimate Service v

Dec 30, 2022
A starter repo for VS Code, Gitpod, etc for Go.

go-starter After using this template: Replace github.com/mattwelke/go-starter in go.mod after using this template. Replace go-starter in .gitignore wi

Nov 26, 2021
Source code related to an on-site demonstration (for Ardan Labs) of packaging Go applications with Nix

Ardan Labs Nix Demo High-Level Overview We bumbled the scheduling of an earlier presentation about git worktree that a few co-workers attended. In eff

Oct 12, 2022
The starter code for Module 3: Surfstore

Surfstore This is the starter code for Module 3: Surfstore. Before you get start

Feb 13, 2022
Golang client for Ethereum and Flashbots JSON-RPC API calls.

Flashbots RPC client Fork of ethrpc with additional Flashbots RPC methods: FlashbotsGetUserStats FlashbotsCallBundle FlashbotsSendBundle FlashbotsSimu

Jan 5, 2023
CRUD API server of Clean Architecture with Go(Echo), Gorm, MySQL, Docker and Swagger
CRUD API server of Clean Architecture with Go(Echo), Gorm, MySQL, Docker and Swagger

CRUD API Server of Clean Architecture Go(echo) gorm mysql docker swagger build docker-compose up -d --build API Postman and Fiddler is recommended to

May 30, 2021