As part of the upgrade to version 2 of the Brightbox Go API I’ve been using some of the newer features Go has introduced over the past few years: generators, generics and error wrapping.
Today we’ll be looking at Go enumerations, why they are useful, what you have to do to map them in and out of JSON, and how to use Go’s code generation capabilities to create them easily when required.
Version 1 of the Brightbox Go API is a very thin wrapper over a HTTP API call that decodes the returned JSON objects into structures using strings and integers. The Brightbox API has lots of enumerations for various attributes, with the main one being a status field which tells you what state an API object is in.
With version 1 this is a string comparison
if server.Status == "deleting" {
return false, nil
}
Which is all fine until you have a senior moment and forget how to spell
if server.Status == "deleteing" {
return false, nil
}
The compiler can’t help you here and you have an annoying bug that is difficult to track down.
Of course more than one type of object can be deleting, and since the status fields are all strings, it’s easy to get them mixed up
currentStatus := serverGroup.Status
// Sometime later...
if server.Status == currentStatus {
return false, nil
}
Once again the compiler can’t help you out here, and it gets even worse when you introduce generic functions that are expected to take, and infer, the types of their arguments. It becomes very easy to mix things up in the code.
Really we want the compiler to complain when we spell things incorrectly or test a server group against a server. How can we get the Go type system to help us out?
Go doesn’t have Enumeration types as such. Instead it fakes them via defined integer types and typed constants using auto generated integers.
package proxyprotocol
import (
"encoding/json"
"fmt"
"reflect"
)
// Enum is an enumerated type
type Enum uint8
const (
// V1 is an enumeration for proxyprotocol.Enum
V1 Enum = iota + 1
// V2 is an enumeration for proxyprotocol.Enum
V2
// V2Ssl is an enumeration for proxyprotocol.Enum
V2Ssl
// V2SslCn is an enumeration for proxyprotocol.Enum
V2SslCn
)
Here we see a few of the tricks used to make a good enumeration in Go.
proxyprotocol.Enum
reads properly in any code that uses itEnum
type uses the smallest unsigned integer type necessary
to hold the enumerations and is declared as a defined type, creating
a new separate type, rather than an aliasiota
to automatically generate unique incrementing integer valuesNow the compiler can catch spelling errors
$ go test
# github.com/terraform-providers/terraform-provider-brightbox/brightbox
brightbox/resource_brightbox_server.go:439:36: undefined: serverstatus.Deleteing
and incorrect comparisons
$ go test
# github.com/terraform-providers/terraform-provider-brightbox/brightbox
brightbox/resource_brightbox_server.go:440:23: invalid operation: obj.Status == loadbalancerstatus.Deleting (mismatched types serverstatus.Enum and loadbalancerstatus.Enum)
brightbox/resource_brightbox_server.go:441:17: invalid operation: obj.Status == loadbalancerstatus.Failed (mismatched types serverstatus.Enum and loadbalancerstatus.Enum)
However we can’t yet represent the enumeration as a string
./example_test.go:13:21: status.String undefined (type serverstatus.Enum has no field or method String)
for that we need to implement the Stringer
interface.
Go has a handy mechanism to call tools referenced within the source
code to generate other source files. The tools library has a command
called stringer
which can create the String
function automatically for
an Enumeration type.
First you install the tool
$ go install golang.org/x/tools/cmd/stringer@latest
Then you add a magic comment to your enumeration code
package serverstatus
//go:generate stringer -type Enum
// Enum is an enumerated type
type Enum uint8
const (
// Creating is an enumeration for serverstatus.Enum
Creating Enum = iota + 1
...
and then you generate the code
$ go generate
This creates a file called enum_string.go
which contains the String
function. Now we can see what it does.
$ go test
--- FAIL: Example (0.00s)
got:
Deleting
want:
deleting
FAIL
exit status 1
Unfortunately stringer generates an exact textual copy of the constant as it is in the source code, which is fine for generating human consumed output, but no good for the JSON string we need. The Brightbox API expects snake case, not camel case in the enumerations.
So instead of tackling the Stringer
interface directly, let’s delegate
the problem to the JSON level within the TextMarshaler
interface, and
then the String
function becomes straightforward and universal
// String makes Enum satisfy the Stringer interface
func (i Enum) String() string {
tmp, err := i.MarshalText()
if err == nil {
return string(tmp)
}
return ""
}
Go handles encoding a type to and from JSON string format via two
interfaces: the TextMarshaler
interface which encodes the type as a string
and the TextUnmarshaler
interface which decodes a string into the type.
Annoyingly the two interfaces are asymmetric in the way they handle
errors. The marshalling system will wrap an error returned by
TextMarshal
in a MarshallerError
type, whereas the unmarshal system
leaves it up to the TextUnmarshaler
implementer to wrap any errors in
an UnmarsalTypeError
.
Implementing the TextMarshal
function is handled by a simple switch
and error return.
// MarshalText implements the text marshaller method
func (i Enum) MarshalText() ([]byte, error) {
switch i {
case Creating:
return []byte("creating"), nil
case Active:
return []byte("active"), nil
case Inactive:
return []byte("inactive"), nil
case Deleting:
return []byte("deleting"), nil
case Deleted:
return []byte("deleted"), nil
case Failed:
return []byte("failed"), nil
case Unavailable:
return []byte("unavailable"), nil
}
return nil, fmt.Errorf("%d is not a valid serverstatus.Enum", i)
}
The return direction is broken out into a standalone function that can create a type directly from a string
// ParseEnum attempts to convert a string into a Enum
func ParseEnum(name string) (Enum, error) {
switch name {
case "creating":
return Creating, nil
case "active":
return Active, nil
case "inactive":
return Inactive, nil
case "deleting":
return Deleting, nil
case "deleted":
return Deleted, nil
case "failed":
return Failed, nil
case "unavailable":
return Unavailable, nil
}
var zero Enum
return zero, fmt.Errorf("%s is not a valid serverstatus.Enum", name)
}
The TextUnmarshaler
interface can then leverage ParseEnum
in a standard
fashion and wrap the error return appropriately, in the required
UnmarshalText
method
// UnmarshalText implements the text unmarshaller method
func (i *Enum) UnmarshalText(text []byte) error {
name := string(text)
tmp, err := ParseEnum(name)
if err != nil {
return &json.UnmarshalTypeError{
Value: name,
Type: reflect.TypeOf(*i),
}
}
*i = tmp
return nil
}
Now we have the layout for Go enumerations, it’s but a small step
to create a generator that will write the enumerations for us. The
generate_enum
script does just that. It takes the package name and a
list of items in the enumeration as arguments and creates a new package
in the enums
directory relative to the package file containing the
generate command. There’s even a -z
option which will make the
first time in the enumeration list the ‘zero value’ that is assigned
automatically when a variable is declared.
It’s then trivial to generate a set of enumerations
//go:generate ./generate_enum loadbalancerstatus creating active deleting deleted failing failed
//go:generate ./generate_enum proxyprotocol v1 v2 v2-ssl v2-ssl-cn
//go:generate ./generate_enum balancingpolicy least-connections round-robin source-address
//go:generate ./generate_enum healthchecktype tcp http
//go:generate ./generate_enum listenerprotocol tcp http https
import them for use
import (
"context"
"path"
"time"
"github.com/brightbox/gobrightbox/v2/enums/balancingpolicy"
"github.com/brightbox/gobrightbox/v2/enums/healthchecktype"
"github.com/brightbox/gobrightbox/v2/enums/listenerprotocol"
"github.com/brightbox/gobrightbox/v2/enums/loadbalancerstatus"
"github.com/brightbox/gobrightbox/v2/enums/proxyprotocol"
)
and declare typed Enum items
// LoadBalancerListener represents a listener on a LoadBalancer
type LoadBalancerListener struct {
Protocol listenerprotocol.Enum `json:"protocol,omitempty"`
In uint16 `json:"in,omitempty"`
Out uint16 `json:"out,omitempty"`
Timeout uint `json:"timeout,omitempty"`
ProxyProtocol proxyprotocol.Enum `json:"proxy_protocol,omitempty"`
}
If you want to play with the Brightbox API, you can sign up for Brightbox real quick and use your £50 free credit to give it a go.