diff --git a/src/compact_encoding/__init__.py b/src/compact_encoding/__init__.py index 2c84a36..23d336e 100644 --- a/src/compact_encoding/__init__.py +++ b/src/compact_encoding/__init__.py @@ -23,6 +23,7 @@ ) from .integer import int_codec as int from .json import json_codec as json +from .record import record from .state import State from .string import utf8 from .string import utf8 as string @@ -49,6 +50,7 @@ "int48", "int56", "json", + "record", "string", "uint", "uint8", diff --git a/src/compact_encoding/record.py b/src/compact_encoding/record.py new file mode 100644 index 0000000..7b7f5d0 --- /dev/null +++ b/src/compact_encoding/record.py @@ -0,0 +1,31 @@ +from . import integer + + +class _Record: + def __init__(self, key_codec, value_codec): + self._key = key_codec + self._value = value_codec + + def preencode(self, state, v): + integer.uint.preencode(state, len(v)) + for k, value in v.items(): + self._key.preencode(state, k) + self._value.preencode(state, value) + + def encode(self, state, v): + integer.uint.encode(state, len(v)) + for k, value in v.items(): + self._key.encode(state, k) + self._value.encode(state, value) + + def decode(self, state): + n = integer.uint.decode(state) + out = {} + for _ in range(n): + key = self._key.decode(state) + out[key] = self._value.decode(state) + return out + + +def record(key_codec, value_codec): + return _Record(key_codec, value_codec) diff --git a/tests/test_record.py b/tests/test_record.py new file mode 100644 index 0000000..77c3ae9 --- /dev/null +++ b/tests/test_record.py @@ -0,0 +1,32 @@ +import pytest + +import compact_encoding as cenc + +# record(string, uint): uint(count) + (key, value) pairs in insertion order. +# Vectors from hyperschema-test fixture 23. +RECORD_CASES = [ + ({}, "00"), + ({"x": 0}, "01017800"), + ({"alice": 10, "bob": 20}, "0205616c6963650a03626f6214"), +] + + +@pytest.mark.parametrize("value,hexbytes", RECORD_CASES) +def test_record_encode(value, hexbytes): + assert cenc.encode(cenc.record(cenc.string, cenc.uint), value).hex() == hexbytes + + +@pytest.mark.parametrize("value,hexbytes", RECORD_CASES) +def test_record_decode(value, hexbytes): + assert ( + cenc.decode(cenc.record(cenc.string, cenc.uint), bytes.fromhex(hexbytes)) + == value + ) + + +def test_record_preserves_insertion_order_not_sorted(): + # {"b": 2, "a": 1} must encode b before a (insertion order), NOT sorted. + # count 02 + "b"(0162)+uint2(02) + "a"(0161)+uint1(01) + codec = cenc.record(cenc.string, cenc.uint) + assert cenc.encode(codec, {"b": 2, "a": 1}).hex() == "02016202016101" + assert cenc.decode(codec, bytes.fromhex("02016202016101")) == {"b": 2, "a": 1} diff --git a/tests/test_roundtrip.py b/tests/test_roundtrip.py index 824e172..979303c 100644 --- a/tests/test_roundtrip.py +++ b/tests/test_roundtrip.py @@ -33,6 +33,7 @@ (cenc.json, {"a": [1, 2], "b": None}), (cenc.frame(cenc.uint), 42), (cenc.array(cenc.uint), [1, 2, 3]), + (cenc.record(cenc.string, cenc.uint), {"a": 1, "b": 2}), ]