Encoding and Decoding
Much like Ribs JSON library, Ribs lets you define binary Codec
s that make it
very easy to get complete control over decoding and encoding your Dart objects
to binary data.
Let's start with a hypothical set of models.
final class Document {
final Header header;
final IList<Message> messages;
const Document(this.header, this.messages);
}
final class Header {
final double version;
final String comment;
final int numMessages;
const Header(this.version, this.comment, this.numMessages);
}
sealed class Message {}
final class Info extends Message {
final String message;
Info(this.message);
}
final class Debug extends Message {
final int lineNumber;
final String message;
Debug(this.lineNumber, this.message);
}
Codec
To start, we'll define a Codec
for the Info
and Debug
classes:
final infoCodec =
Codec.utf16_32.xmap((str) => Info(str), (info) => info.message);
final debugCodec = Codec.product2(
Codec.int32L, // 32-bit little endian int
Codec.ascii32, // ascii bytes with 32bit size prefix
Debug.new,
(dbg) => (dbg.lineNumber, dbg.message),
);
The infoCodec
uses the utf16_32
codec which prefixes a 32-bit integer,
indicating the length of the encoded string and then the string itself
using a UTF16 encoding.
The debugCodec
needs to use 2 different codecs to properly encode/decode
the 2 fields from the Debug
class:
int32L
: Serializes theint
as little endian using 32-bitsascii32
: Serializes the string using ASCII, while prepending a 32-bit integer to indicate the length of the string.
Next, we'll define a Codec
for Message
, the superclass of Info
and Debug
:
final messageCodec = Codec.discriminatedBy(
Codec.uint8, // encode identifier using an 8-bit integer
imap({
0: infoCodec, // instances of Info prefixed by ID 0
1: debugCodec, // instances of Debug prefixed by ID 1
}),
);
Here we use discriminatedBy
to allow us to properly encode and decode
instances of Message
by prefixing an unique indentifier tag before each
message. In this particular instance, that tag is an 8-bit integer.
The only pieces left are the codecs for Header
and Document
:
final documentCodec = Codec.product2(
headerCodec,
// ilist of Messages with 16-bit int prefix indicating # of elements
Codec.ilistOfN(Codec.int16, messageCodec),
Document.new,
(doc) => (doc.header, doc.messages),
);
final headerCodec = Codec.product3(
Codec.float32, // 32-bit floating point
Codec.utf8_32, // utf8 bytes with 32bit size prefix
Codec.int64, // 64-bit integer
(version, comment, numMessages) => Header(version, comment, numMessages),
(hdr) => (hdr.version, hdr.comment, hdr.numMessages),
);
Finally let's see how you can use the Codec
to encode and decode binary data:
final doc = Document(
const Header(1.1, 'Top Secret', 3),
IList.fromDart([
Info('Hello!'),
Debug(123, 'breakpoint-1'),
Info('Goodbye!'),
]));
// Encoding will give us an error or the successfully encoded BitVector
final Either<Err, BitVector> bits = documentCodec.encode(doc);
print(bits);
// Right(3f8ccccd00000050546f702053656372657400000000000000030003000000003048656c6c6f21017b00000000000060627265616b706f696e742d310000000040476f6f6462796521)
// Decoding will give us either an error if it failed or the DecodeResult
// A DecodeResult gives us the successfully decoded value and any remaining bits from the input
// ** Note the throw is included only for edification purposes. This is not a good idea in production code
final Either<Err, DecodeResult<Document>> decoded = documentCodec
.decode(bits.getOrElse(() => throw Exception('encode failed!')));
print(decoded);
// Right(DecodeResult(Instance of 'Document', ByteVector.empty))
The example above illustrates a successful encoding of a Document
and then a
successful decoding of those previously encoded bits.