Go CBOR encoder: Episode 2, booleans
In the previous episode, we learned how to encode the nil value. Now we’ll do booleans. According to the CBOR specification, booleans are represented by a single byte: 0xf4 for false, and 0xf5 for true.
We’ll write the tests first, but before we do that let’s write a helper function for our encoder tests. We want to avoid copy-pasting the same code all over our tests. Looking at the test we wrote in the previous episode, that’s how all of our future tests will look like:
func TestNil(t *testing.T) {
var buffer = bytes.Buffer{}
var err = NewEncoder(&buffer).Encode(nil)
if !(err == nil && bytes.Equal(buffer.Bytes(), []byte{0xf6})) {
t.Fatalf(
"%#v != %#v or %#v != %#v",
err, nil, buffer.Bytes(), []byte{0xf6},
)
}
}
We test something with a well defined interface: the encoder gets a value, returns an error, and outputs an array of bytes. This means we can use this to factor out most of the code into a single helper function named testEncoder, we add this to our test file:
// testEncoder test the CBOR encoder with the value v, and verify that err, and
// expected match what's returned and written by the encoder.
func testEncoder(t *testing.T, v interface{}, err error, expected []byte) {
// buffer is where we write the CBOR encoded values
var buffer = bytes.Buffer{}
// create a new encoder writing to buffer, and encode v with it
var e = NewEncoder(&buffer).Encode(v)
if e != err {
t.Fatalf("err: %#v != %#v with %#v", e, err, v)
}
if !bytes.Equal(buffer.Bytes(), expected) {
t.Fatalf(
"(%#v) %#v != %#v", v, buffer.Bytes(), expected,
)
}
}
testEncoder will save quite a bit of typing. TestNil turns into a single line —saving 8 lines— with testEncoder doing all the work:
func TestNil(t *testing.T) {
testEncoder(t, nil, nil, []byte{0xf6})
}
Let’s write the tests for booleans now. We only need to test true and false, so TestBool is a terse two lines:
func TestBool(t *testing.T) {
testEncoder(t, false, nil, []byte{0xf4})
testEncoder(t, true, nil, []byte{0xf5})
}
With our current encoder only able to encode nil right now, if we run the tests we’ll get a not implemented error thrown at us:
$ go test -v .
=== RUN TestNil
--- PASS: TestNil (0.00s)
=== RUN TestBool
--- FAIL: TestBool (0.00s)
cbor_test.go:19: err: &errors.errorString{s:"Not Implemented"} != <nil> with false
FAIL
FAIL _/home/henry/cbor 0.003s
Now we’ll implement booleans encoding and get those tests passing. From the previous episode our Encode function looked like this:
var ErrNotImplemented = errors.New("Not Implemented")
// Can only encode nil
func (enc *Encoder) Encode(v interface{}) error {
switch v.(type) {
case nil:
var _, err = enc.w.Write([]byte{0xf6})
return err
}
return ErrNotImplemented
}
We need to add another case to the switch block to know if we have a boolean, and then we’ll have to turn the generic interface{} named v into a boolean value to know what the encode will output into its writer:
// Can only encode nil, false, and true
func (enc *Encoder) Encode(v interface{}) error {
switch v.(type) {
case nil:
var _, err = enc.w.Write([]byte{0xf6})
return err
case bool:
var err error
if v.(bool) {
_, err = enc.w.Write([]byte{0xf5}) // true
} else {
_, err = enc.w.Write([]byte{0xf4}) // false
}
return err
}
return ErrNotImplemented
}
The tricky part here is v.(bool): this turns the non-typed interface v into a boolean value using type assertion.
Encode now works with booleans and our tests pass:
$ go test -v .
=== RUN TestNil
--- PASS: TestNil (0.00s)
=== RUN TestBool
--- PASS: TestBool (0.00s)
PASS
ok _/home/henry/cbor 0.003s
This wraps up the 2nd episode. Next we’ll encode a type that’s more than a single byte of output: positive integers.
I created a public repository with the code for this episode.