sWAGger - Web API Generator.
A custom Web API Generator written by Clever.
Despite the presence of a swagger.yml
file, WAG does not support all of the Swagger standard.
WAG is a custom re-implementation of a subset of the Swagger version 2.0
standard.
Wag requires Go 1.21+ to build, and the generated code also requires Go 1.21+.
The code generated by wag
imposes dependencies that you should include in your go.mod
. The go.mod
file under samples/
provides a list of versions that definitely work; pay special attention to the versions of go.opentelemetry.io/*
, github.com/go-swagger/*
, and github.com/go-openapi/*
.
Create a swagger.yml file with your service definition. Wag supports a subset of the Swagger spec.
Copy the latest wag.mk
from the dev-handbook.
Set up a generate
target in your Makefile
that will generate server and client code:
include wag.mk
WAG_VERSION := latest
generate: wag-generate-deps
$(call wag-generate-mod,./swagger.yml, $(PKG))
wag.mk
requires WAG_VERSION
to be set. It can be either latest
or a tagged release like v8.0.0
. latest
will use the most recent non-prerelease.
Define global BadRequest and InternalError response types. These are used internally for validation errors and unknown errors respectively. They must reference a definition with a message field. For example:
responses:
BadRequest:
description: Bad Request
schema:
$ref: "#/definitions/BadRequest"
InternalError:
description: Internal Error
schema:
$ref: "#/definitions/InternalError"
definitions:
BadRequest:
type: object
properties:
message:
type: string
InternalError:
type: object
properties:
message:
type: string
For more information on error definitions see the Errors section below.
Then generate your code:
make generate
This generates four directories. You should not have to modify any of the generated code:
- gen-go/models: contains all the definitions in your Swagger file as well as the API input / output definitions
- gen-go/server: contains the router, middleware, and handler logic
- gen-go/client: contains the Go client library
- gen-go/tracing: contains the code for both client and server for adding OpenTelemetry instrumentation.
- gen-js: contains the javascript client library
To implement and run the generated server you need to:
- Implement the controller interface defined in
gen-go/server/interface.go
- Pass the controller into the Server constructor. For example:
s := server.New(myController, ":8000")
// Serve should not return
log.Fatal(s.Serve())
Or, with custom middleware:
s := server.NewWithMiddleware(myController, ":8000", []func(http.Handler) http.Handler{
myFirstMiddlware, mySecondMiddlware})
// Serve should not return
log.Fatal(s.Serve())
The server interface defined in gen-go/server/interface.go
has one method for each operation defined in the swagger.yml. We generate the interface based on the following rules:
- The first argument to each Wag operation is a
context.Context
. See below for more details on how Wag uses contexts. - If one parameter is defined then Wag uses that input directly in the function definition.
func F(ctx context.Context, input string) error
- If more than one parameter is defined then Wag generates a input struct with all the parameters:
func F(ctx context.Context, input *models.{{OperationID}}Input) error
- Optional parameters that don't have defaults are pointers in the input struct so that the server can distinguish between parameters that aren't set and parameters that are set to the zero value.
- Wag only supports defining a single 2XX response status code and doesn't support 3XX status codes.
- If the success response type has a data type associated with it then Wag generates an interface that takes a pointer to that data type as the first argument.
func(...) (*SuccessType, error)
- If the operation uses
x-paging
, then Wag generates the an interface that takes the type of the page ID parameter as the second return type.func(...) (*SuccessType, PageParamType, error)
- If the success response type doesn't define a data type then Wag generates an interface with only an error response. A nil error tells the client that the request succeeded.
func(...) error
- If the success response type has a data type associated with it then Wag generates an interface that takes a pointer to that data type as the first argument.
The kayvee middleware logger is automatically added to the context object.
It can be pulled out of the context object and used via the kayvee FromContext
method:
import "gopkg.in/Clever/kayvee-go.v6/logger"
...
logger.FromContext(ctx).Info(...)
You should use this logger for all logging within your controller implementation.
Note: This is an internal Clever feature. Ignore this if you are using wag
outside of Clever (also, hi!)
wag
is already set up for log routing if you so wish. To set up application log routing in your service, add your kvconfig.yml
file to the same directory as your service executable. e.g. in your Dockerfile:
COPY kvconfig.yml /usr/bin/kvconfig.yml
COPY bin/my-wag-service /usr/bin/my-wag-service
-
Wag supports three types of errors
- Global error response types
- Response types for a specific operation
- Unexpected errors
-
Any of these can be returned from a controller. To return a global or response specific error type return a pointer to the model defintion for that error type. To return an unexpected error return any Go error. Wag automatically converts errors not defined swagger yml into the default 500 response.
-
All error responses defined in the swagger yml must have a
Message
field. The field is used as the return value of theError()
for the corresponding Go error type. -
Wag has two built-in errors:
#/definitions/BadRequest
(400) and '#/responses/InternalError' (500). Any operation that doesn't explicitly define a 400 and/or 500 response gets these automatically so Wag can use them to return validation and internal errors respectively. -
Errors returned from your controller are logged by the autogenerated handler code, so there is no need to separately log errors yourself. If you use the
github.com/go-errors/errors
package, the stacktrace will also be logged, making debugging easier.
For undefined error types the best practices are:
* If you receive an error from an external dependency, use
errors.WrapPrefix(err, "foopackage.func", 0)
to return an error with a
stacktrace and prefix the source of the error (in this example, we received
an error from the function func
in the package foopackage
).
- If you generate a new error, use
errors.Errorf
orerrors.New
to build the error. - If you receive an error from an internal function, just return the error
directly since it should already have stacktrace information (either it is
a wrapped external error or a
go-errors
-generated internal error).
- Wag supports four types of parameters
- Path parameters
- Must be required
- Must be a simple type (e.g. string, integer)
- Will not be pointers
- Body parameters
- Must reference a 'definition' schema
- Will be pointers
- Cannot have defaults
- Query parameters
- Must be a simple or array type. If an array must be an array of strings
- If the type is 'simple' and the parameter is not required, the type will be a pointer
- If the type is 'array' it won't be a pointer. Query parameters can't distinguish between an empty array and an nil array so it converts both these cases to a nil array. If you need to distinguish between the two use a body parameter
- In other cases the parameter is not a pointer
- Header parameters
- Must be simple types
- If marked required will ensure that the input isn't the nil value. Headers cannot have pointer types since HTTP doesn't distinguish between empty and missing headers.
- If it doesn't have a default value specified, the default value will be the nil value for the type
- Path parameters
- Wag can help implement paging on endpoints if you use the
x-paging
configuration:/books: operationId: getBooks x-paging: pageParameter: startingAfter
pageParameter
should be set to the name of a parameter you define on the operation that specifies a page ID. The parameter can be a query, header, or path parameter (it may not be the only path parameter).- You can also specify
resourcePath
if the array you want iterate over isn't the top-level success return for the operation. (See/authors
in samples/swagger.yml.) - The server interface will be amended with a 2nd success return type so that your controller can provide the next page ID the client should fetch.
- The autogenerated Go client will include a
New<OperationID>Iter
function that returns an iterator object that exposes a Next() function that will return successive resources, requesting new pages as needed. - The autogenerated JS client will include an
<operationID>Iter
function that exposesmap
,forEach
,forEachAsync
andtoArray
functions to iterate over the results, again requesting new pages as needed.
-
The first argument to every Wag function is a
context.Context
(https://blog.golang.org/context). Contexts play a few important roles in Wag.- They can be used to set request specific behavior like a retry policy in client libraries. This includes timeouts and cancellation.
- They are used to pass metadata, like tracing information, across API requests.
-
To get these benefits pass the context object to any subsequent network requests you make. Many client libraries accept the context object, e.g.:
- net/http: If you're making HTTP requests, use the golang.org/x/net/context/ctxhttp package.
- wag If your handler consumes a
wag
-generated client, then pass the context object to these client methods.
-
If you don't have a context to pass to a Wag function you have two options
- context.Background() - use this when this is the creator of the request chain, like a test or a top-level service.
- context.TODO() - use this when you haven't been passed a context from a caller yet, but you expect the caller to send you one at some point.
- Wag can auto-generate server code to save models to DynamoDB if you specify the
x-db
extension on a schema:
definitions:
Thing:
x-db:
AllowOverwrites: false
DynamoDB:
KeySchema:
- AttributeName: name
KeyType: HASH
- AttributeName: version
KeyType: RANGE
type: object
properties:
name:
type: string
version:
type: integer
The above will generate a db
package with code to load/persist Thing
objects from/to DynamoDB.
AllowOverwrites
specifies whether the auto-generatedSave
method should succeed if the object already exists.DynamoDB
specifies the configuration for a DyanmoDB table for the schema. It follows the format of theAWS::DynamoDB::Table
CloudFormation resource. Currently it supports a subset of the configuration allowed there.
wag
instruments servers and clients with opentelemetry-go. You must initialize tracing before use with a snippet like this in your main()
:
// in your imports
import <MY REPO NAME>/gen-go/tracing"
// in your main
exp, prov, err := tracing.SetupGlobalTraceProviderAndExporter(context.Background())
if err != nil {
log.Fatalf("failed to setup tracing: %v", err)
}
defer exp.Shutdown(context.Background())
defer prov.Shutdown(context.Background())
In order for the defer
statements to work properly, this should be called in main()
or the same function that you call Serve()
to start the server.
Server instrumentation is done via go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux.
Client instrumentation is done via go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp.
Traces are sent to opentelemetry-collector running at localhost:4317
.
If running locally, all samples are traced.
Otherwise, the probability of sampling is 0.01 (i.e. one in one hundred traces) and can be overriden by setting the environment variable TRACING_SAMPLING_PROBABILITY
.
These details (the function to setup tracing, the addresses to which traces are sent, and the TRACING_SAMPLING_PROBABILITY
variable) are stable for 7.x, but other details of tracing, such as the instrumention and the tags that are sent, are subject to changes even within 7.x.
In addition, tracing relies on a specific version of the opentelemetry
modules; you can find a complete list of dependencies that work in samples/go.mod
.
Initialize the client with New
c := client.New("https://url_of_your_service:port")
Make an API call
books, err := c.GetBooks(ctx, GetBookByIDInput{Authors: []string{"Twain"}})
if err != nil {
// Do something with the error
}
If you're using the client from another WAG-ified service you should pass in the ctx
object you get in your server handler. Otherwise you can use context.Background()
We've added custom string validation for mongo-ids to avoid repeating: "^[0-9a-f]{24}$"` throughout the swagger.yml. To use it you have must:
- Change you swagger.yml file to have the
mongo-id
format. For example:
authorID:
type: string
format: mongo-id
- Import
github.com/Clever/wag/swagger
and callswagger.InitCustomFormats()
in your server code.
Note that custom string validation only applies to input parameters and does not have any impact on objects defined in '#/definitions'.
Right now we do not allow user-defined custom strings, but this is something we may add if there's sufficient demand.
You can initialize the client by either passing a url or by using discovery.
import * as SampleClientLib from '@clever/sample-client-lib-js';
const sampleClient = new SampleClientLib({address: "https://url_of_your_service:port"}); // Explicit url
// OR
const sampleClient = new SampleClientLib({discovery: true}); // Using discovery
You may also configure a global timeout for requests when initalizing the client.
const sampleClient = new SampleClientLib({discovery: true, timeout: 1000}); // Timeout any requests taking longer than 1 second
You may then call methods on the client. Methods support callbacks and promises.
// Promises
sampleClient.getBookById("bookID").then((book) => {
// ...
}).catch((err) => {
// ...
});
// Callbacks
sampleClient.getBookById("bookID", (err, book) => {
// ...
});
You can also pass an optional options argument. This can have the following options
timeout
- overide the global timeout for this specific call
const options = {
timeout: 5000 // Timeout after 5 seconds
}
sampleClient.getBookById("bookID", options, (err, book) => {
// ...
});
As of v7, wag
no longer provides any special tracing experience for Javascript clients. We recommend using the @opentelemetry
packages to add client-side instrumentation.
make test
Currently, Wag doesn't implement the entire Swagger Spec. A couple things to keep in mind:
- All schemas should reference type definitions in /definitions. Any schemas defined in /paths will cause an error.
- Scheme, produces, and consumers can only be defined in the top-level swagger object, not individual operations. On the top level object the scheme must be 'http', produces must be 'application/json' and consumes must be 'application/json'
Below is a more comprehensive list of the features we don't yet support
Mime Types
Multi-File Swagger Definitions
Schema:
- host
- tags
- scheme (must be http)
- consumes
- produces
- securityDefinitions
- security
Consumes:
- produces (must be application/json)
- consumes (must be application/json)
- schemes
- security
Form parameter type Parameter:
- file parameter type
- collectionFormat
- global definitions
- possibly the json schema requirements? (uniqueItems, multipleOf, etc...)
Schema object (all these have to be defined in /definitions and are generated by go-swagger)
Discriminators
XML Modeling
Security Objects
Response:
- Headers
The New()
and NewWithMiddleware()
will return a Server
that can start an HTTP server with
the endpoints defined in the swagger yml. If you want to extend the autogenerated endpoints or
if you need to make use of the ResponseWriter
or Request
within a given endpoint, you can
use NewRouter()
to expose the *mux.Router
and then AttachMiddleware()
to attach the
wag middleware and return a Server
. For example:
// Create new controller with all the swagger functions implemented
c := controller.New()
// Create new *mux.Router
routerHandler := server.NewRouter(c)
// Inject our own authorize endpoint to wrap around the swagger handler
router.Methods("GET").Path("/authorize").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
input, _ := sanitizeAuthorizeInput(r)
resp, _ := c.AuthorizeUserForApp(r.Context(), input)
http.Redirect(w, r, fmt.Sprintf("%s", *resp.RedirectURI), 302)
})
// Make and start server
s := server.MakeServer(router, *addr, middleware)
if err := s.Serve(); err != nil {
log.ErrorD("error-server-setup", logger.M{
"error": err.Error(),
})
os.Exit(1)
}
The created /authorize
endpoint uses the autogenerated AuthorizeUserForApp
function, but will
make use of the ResponseWriter
and Request
to redirect the caller to the RedirectURI
The following directories and files are generated and should not be manually edited:
- samples/gen-*/*
- hardcoded/hardcoded.go
Once you've made your changes, run make test
and check that the generated code looks ok, then check in those changes.