Index

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.