~chiefnoah/pybare

592bbb8d310971ae3ec755353d36f51f4a649ab5 — Noah Pederson 8 months ago eac7271
Implements all container and primitive types
3 files changed, 848 insertions(+), 85 deletions(-)

M bare/encoder.py
M bare/test_encoder.py
M bare/types.py
M bare/encoder.py => bare/encoder.py +372 -57
@@ 2,15 2,19 @@ import io
import logging
import struct
import typing
import inspect
from abc import ABC, abstractmethod
from collections import OrderedDict
from enum import Enum, auto
from functools import partial
from collections.abc import Mapping
from collections import UserDict, UserList


class ValidationError(ValueError):
    pass


class BareType(Enum):
    UINT = auto()
    U8 = auto()


@@ 78,6 82,7 @@ class Field(ABC):

    def __set_name__(self, owner, name):
        self.name = name
        setattr(owner, f"_{name}", self._value)

    def __get__(self, instance, owner=None):
        if instance is None:


@@ 87,15 92,21 @@ class Field(ABC):

    def __set__(self, instance, value):
        if instance is None:
            raise AttributeError("Unable to assign value when not attached to class")
        if not self.validate(value):
            raise ValidationError(f"{value} is invalid for BARE type {self._type}")
            raise AttributeError("Unable to assign value when not attached to object")
        valid, message = self.validate(value)
        if not valid:
            raise ValidationError(
                f"value is invalid for BARE type {self._type}: {message}"
            )
        setattr(instance, f"_{self.name}", value)

    @classmethod
    @abstractmethod
    def validate(cls, value) -> bool:
        return cls._default == None  # This is valid for BareType.Void
    def validate(self, value) -> typing.Tuple[bool, str]:
        """
        Checks whether a give value is valid for the Field's data type. Returns a tuple of a boolean
        and an optional message for why
        """
        return self.__class__._default == None  # This is valid for BareType.Void

    @property
    def type(self):


@@ 111,33 122,41 @@ class Field(ABC):
            raise ValidationError(f"{value} is invalid for BARE type {self._type}")
        self._value = value

    @abstractmethod
    def _pack(self, fp, value=None):
        pass

    @classmethod
    @abstractmethod
    def _unpack(cls, fp: typing.BinaryIO) -> "Field":
        pass

    def pack(self, fp=None) -> typing.Optional[bytes]:
        buffered = False
        if not fp:
            fp = io.BytesIO()
            buffered = True
        _dump(fp, self, self.value)
        self._pack(fp)
        if buffered:
            return fp

    @classmethod
    def unpack(cls, fp: typing.BinaryIO) -> "Field":
    def unpack(cls, fp: typing.BinaryIO):
        # If it's a bytes-like, wrap it in a io buffer
        if hasattr(fp, "decode"):
            fp = io.BytesIO(fp)
        if primitive_types.get(cls._type) is not None:
            value = primitive_types.get(cls._type)[1](fp)
            return cls(cls._type, value)
        return cls._unpack(fp)


class Struct(ABC):

    _type = BareType.Struct

    def __init__(self, *args, **kwargs):
    def __init__(self, *args, optional=False, **kwargs):
        self._optional = optional
        # loop through defined fields, if they have a corresponding kwarg entry, set the value
        for name, field in filter(
            lambda x: isinstance(x[1], Field), self.__class__.__dict__.items()
            lambda x: isinstance(x[1], (Field, Struct)), self.__class__.__dict__.items()
        ):
            if name in kwargs:
                setattr(self, name, kwargs[name])


@@ 147,7 166,7 @@ class Struct(ABC):
    @classmethod
    def fields(cls) -> typing.OrderedDict[str, Field]:
        return OrderedDict(
            filter(lambda x: isinstance(x[1], Field), cls.__dict__.items())
            filter(lambda x: isinstance(x[1], (Field, Struct)), cls.__dict__.items())
        )

    def pack(self, fp=None) -> typing.Optional[bytes]:


@@ 160,11 179,29 @@ class Struct(ABC):
        if not fp:
            fp = io.BytesIO()
            ret = True
        for field, type in self.fields().items():
            _dump(fp, type, getattr(self, field))
        self._pack(fp)
        if ret:
            return fp.getvalue()

    def _pack(self, fp: typing.BinaryIO, value=None):
        if value is None:
            value = self
        for field, type in value.fields().items():
            val = getattr(self, field)  # this gets the underlying value
            type._pack(fp, value=val)

    @classmethod
    def _unpack(cls, fp: typing.BinaryIO):
        vals = {}
        for field, type in cls.fields().items():
            if isinstance(type, Array):
                val = type._unpack(fp, length=type._length)
            else:
                val = type._unpack(fp)
            vals[field] = val.value
        return cls(**vals)


    @classmethod
    def unpack(cls, data: typing.Union[typing.BinaryIO, bytes]):
        """


@@ 174,51 211,329 @@ class Struct(ABC):
            fp = io.BytesIO(data)
        else:
            fp = data
        # for field, type in cls.fields().items():
        #    _load(fp, type)
        return _load(fp, cls)


class Optional(ABC):
    pass
        return cls._unpack(fp)

    @property
    def value(self):
        # A structs value is itself
        return self

    def validate(self, s) -> typing.Tuple[bool, str]:
        try:
            for name, field in s.fields().items():
                valid, message = field.validate(getattr(s, name))
                if not valid:
                    return False, message
        except AttributeError:
            return False, f"{type(s)} is not a valid struct {type(self)}"
        return True, None


class _ValidatedList(UserList):
    def __init__(self, *args, instance: "Array" = None, **kwargs):
        if instance is None:
            raise ValueError(
                "Must specify backreference to Array. This is likely a bug in the library."
            )
        self._instance = instance
        super().__init__(*args, **kwargs)

    def append(self, item):
        valid, message = self._instance.validate(len(self.data) + 1, item)
        if not valid:
            raise ValidationError(message)
        self.data.append(item)

    def extend(self, other):
        # This is probably slow, but we get the validation logic from append for free
        for item in other:
            self.append(item)

    def __setitem__(self, i, item):
        valid, message = self._instance.validate(i, item)
        if not valid:
            raise ValidationError(message)
        self.data[i] = item


class Array(Field):

    _type: typing.Type[Field] = None
    _length = 0  # zero means variable length
    _default = None

class Union(ABC):
    def __init__(
        self, type: typing.Type[Field] = None, length=0, values=None
    ):
        if type is not None:
            if inspect.isclass(type):
                self._type = type()
            else:
                self._type = type
        elif self.__class__._type is None:
            raise TypeError(
                "Must either specify type as argument to init or _type class field"
            )
        else:
            self._type = self.__class__._type()
        # if the class _length is specified (in a subclass), it takes precidense over the arg length if it's 0
        if self.__class__._length > 0 and length == 0:
            self._length = self.__class__._length
        else:
            self._length = length
        if values:
            self.validate(values)
            self._value = values
        else:
            self._value = []

    def _validateitem(self, item) -> typing.Tuple[bool, str]:
        if self._length > 0 and len(self._value) + 1 > self._length:
            return False, "outside of length bounds"
        return self._type.validate(item)

    def validate(self, items: typing.Collection) -> typing.Tuple[bool, str]:
        if self._length > 0 and len(items) > self._length:
            return False, f"lenth {len(items)} larger than array max: {cls._length}"
        if self._length > 0 and len(items) > self._length:
            return False, f"length {len(items)} greater than max length {self._length}"
        for item in items:
            valid, message = self._type.validate(item)
            if not valid:
                return False, message
        return True, None

    def _pack(self, fp: typing.BinaryIO, value=None):
        if value is None:
            value = self._value
        if self.__class__._length == 0:
            length = len(value)
            _write_varint(fp, length, signed=False)
        else:
            length = self.__class__.__length
            value = value.extend([self._type._default] * (length - len(value))) # pad with default values
        for item in value:
            self._type._pack(fp, item)

    def _unpack(self, fp: typing.BinaryIO, length=None) -> 'Array':
        if length is None:
            length = cls._length
        if length == 0:
            length = _read_varint(fp, signed=False)
        values = []
        for _ in range(length):
            val = self._type._unpack(fp)
            values.append(val)
        return self.__class__(type=self._type, length=self._length, values=values)

class _ValidatedMap(UserDict):
    def __init__(self, *args, instance: "Map" = None, **kwargs):
        if instance is None:
            raise ValueError(
                "Must specify backreference to Map. This is likely a bug in the library."
            )
        self._instance = instance
        super().__init__(*args, **kwargs)

    _members: typing.Tuple[typing.Union[Field, Struct], ...] = ()
    def __setitem__(self, key, value):
        valid, message = self._instance.validate({key: value})
        if not valid:
            raise ValidationError(message)
        self.data[key] = value

    @property
    def members(self) -> typing.Tuple[typing.Union[Field, Struct], ...]:
        return self.__class__._members
    def update(self, other: Mapping):
        valid, message = self._instance.validate(other)
        if not valid:
            raise ValidationError(f"Unable to update map: {message}")
        self.data.update(other)


class Map(Field):

    _type = BareType.Map
    _key: Field = None
    _value: Field = None
    _default = dict()
    _keytype: typing.Type[Field] = None
    _valuetype: typing.Type[Field] = None
    _default = None

    def __init__(self, default=None):
        if default:
            if not self.__class__.validate(default):
                raise ValidationError(f"{default} is invalid for BARE type {self._type}")
            self._default = default
    def __init__(self, key: Field = None, value: Field = None, values=None):
        if key is not None:
            if inspect.isclass(key):
                self._keytype = key()
            else:
                self._keytype = key
        elif self.__class__._keytype is None:
            raise TypeError(
                "Must either specify key as an argument to init or  _keytype class field"
            )
        else:
            self._keytype = self.__class__._keytype()
        if value is not None:
            if inspect.isclass(value):
                self._valuetype = value()
            else:
                self._valuetype = value
        elif self.__class__._valuetype is None:
            raise TypeError(
                "Must either specify value as an argument to init or  _valuetype class field"
            )
        else:
            self._valuetype = self.__class__._valuetype()
        if values:
            for k, v in values.items():
                valid, message = self._validatekv(k, v)
                if not valid:
                    raise ValidationError(
                        f"Unable to assign value to key: {k}: {message}"
                    )
        self._value = _ValidatedMap(values, instance=self)

    @property
    def value(self):
        return self.__class__._default
    def __set__(self, instance, value):
        if instance is None:
            raise AttributeError("Unable to assign value when not attached to class")
        valid, message = self.validate(value)
        if not valid:
            raise ValidationError(
                f"Attempting to assign invalid value to typed map: {message}"
            )
        wrapped = _ValidatedMap(value, instance=self)
        setattr(instance, f"_{self.name}", wrapped)

    def _validatekv(self, key, value) -> typing.Tuple[bool, str]:
        keyvalid, message = self._keytype.validate(key)
        if not keyvalid:
            return False, f"map key {message}"
        valvalid, message = self._valuetype.validate(value)
        if not valvalid:
            return False, f"map value {message}"
        return True, ""

    def validate(self, value: Mapping) -> typing.Tuple[bool, str]:
        if not isinstance(value, Mapping):
            return False, f"Invalid value type: {type(value)}"
        for k, v in value.items():
            valid, message = self._validatekv(k, v)
            if not valid:
                return False, message
        return True, None

    def _pack(self, fp: typing.BinaryIO, value=None):
        if value is not None:
            value = self._value  # type: _ValidatedMap
        count = len(value)
        _write_varint(fp, count, signed=False)
        for k, v in value.items():
            self._keytype._pack(fp, value=k)
            self._valuetype._pack(fp, value=v)

    @classmethod
    def validate(cls, value: Mapping):
    def _unpack(cls, fp: typing.BinaryIO) -> "Map":
        count = _read_varint(fp, signed=False)
        values = {}
        for _ in range(count):
            key = cls._keytype._unpack(fp)
            value = cls._valuetype.unpack(fp)
            values[key] = value
        return cls(key=cls._keytype, value=cls._valuetype, values=values)


class Optional(Field):
    def __init__(self, wrapped: typing.Union[typing.Type[Field], Field], value=None):
        if inspect.isclass(wrapped):
            if value is not None:
                self._wrapped = wrapped(value)
            else:
                self._wrapped = wrapped()
        else:
            self._wrapped = wrapped
        self._value = value

    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        else:
            return getattr(instance, f"_{self.name}")

    def __set__(self, instance, value):
        if instance is None:
            raise AttributeError("Unable to assign value when not attached to object")
        valid, message = self.validate(value)
        if not valid:
            raise ValidationError(
                f"value is invalid for BARE type {self._type}: {message}"
            )
        setattr(instance, f"_{self.name}", value)

    def validate(self, value):
        if value is None:
            return False
        keytype = cls._key
        valtype = cls._value
        for k, v in value.items():
            if not keytype.validate(k): return False
            if not valtype.validate(v): return False
        return True
            return True, None
        return self._wrapped.validate(value)

    def _pack(self, fp: typing.BinaryIO, value=None):
        if value is None:
            value = self._value
        if value is None:
            fp.write(struct.pack('<B', 0))
        self._wrapped._pack(fp, value=value)

    def _unpack(self, fp: typing.BinaryIO) -> 'Optional':
        buf = fp.read(1)
        check = struct.unpack('<B', buf)[0]
        if check == 0:
            return self.__class__(wrapped=self._wrapped, value=None)
        fp.seek(-1)
        value = self._wrapped._unpack(fp)
        return self.__class__(wrapped=self._wrapped, value=value)

class Union(Field):

    _members: typing.Tuple[typing.Union[Field, Struct], ...] = ()

    def __init__(self, members: typing.Tuple = None, value=None):
        if members is None:
            members = self.__class__._members
        self._members = []
        for member in members:
            if inspect.isclass(member):
                self._members.append(member(value=value))
            else:
                self._members.append(member)
        self._value = value

    @property
    def members(self):
        return self._members

    def validate(self, value) -> typing.Tuple[bool, str]:
        if isinstance(value, Field) and value.__class__ in [
            x.__class__ for x in self._members
        ]:
            return value.validate(value.value)
        for member in self._members:
            valid, _ = member.validate(value)
            if valid:
                return True, None
        return False, f"type {type(value)} is not valid for one of {self._members}"

    def _pack(self, fp: typing.BinaryIO, value=None):
        if value is None:
            value = self._value
        for id, member in enumerate(self._members):
            if isinstance(value, (Field, Struct)):
                # it's a field, so we should try to check for type
                valid = type(member) == type(value)
            else:
                # no idea what it might be, maybe a python native value
                # do the less good validation check
                valid, _ = self.validate(value)
            if valid:
                _write_varint(fp, id, signed=False)
                member._pack(fp, value=value)
                return
        raise TypeError("Unable to determine Union member type for value.")

    def _unpack(self, fp: typing.BinaryIO):
        id = _read_varint(fp, signed=False)
        value = self._members[id]._unpack(fp)
        return self.__class__(value=self._members[id].__class__(value=value.value))


def _write_string(fp: typing.BinaryIO, val: str):


@@ 266,17 581,17 @@ def _read_varint(fp: typing.BinaryIO, signed=True) -> int:


def _dump(fp, field: "Field", val):
    if not isinstance(field, (Field, Struct, Map, Optional, Map)):
    if not isinstance(field, (Field, Struct, Map, Map)):
        raise ValueError(f"Cannot dump non bare.Field: type: {type(val)}")
    if field.type == BareType.String:
    if field._type == BareType.String:
        _write_string(fp, val)
    elif field.type in (BareType.INT, BareType.UINT):
        _write_varint(fp, val, signed=field.type == BareType.INT)
    elif field.type == BareType.Union:
    elif field._type in (BareType.INT, BareType.UINT):
        _write_varint(fp, val, signed=field._type == BareType.INT)
    elif field._type == BareType.Union:
        # must be a composite type, do compisitey things
        # type = next((x for x in )) # TODO: resume here, need UnionType, instance object
        pass
    elif field.type == BareType.Map:
    elif field._type == BareType.Map:
        if not isinstance(val, Mapping):
            raise TypeError(f"You can't to write type {type(val)} as BareType.Map")
        length = len(val)


@@ 287,13 602,13 @@ def _dump(fp, field: "Field", val):
            _dump(fp, field.__class__._key, k)
            _dump(fp, field.__class__._value, v)

    elif primitive_types.get(field.type) is not None:
    elif primitive_types.get(field._type) is not None:
        # it's primitive, use the stored struct.pack method
        b = primitive_types.get(field.type)[0](val)
        b = primitive_types.get(field._type)[0](val)
        fp.write(b)


def _load(fp, field: typing.Union[Field, typing.Type[Struct], Map, Optional]):
def _load(fp, field: typing.Union[Field, typing.Type[Struct], Map]):
    # if not isinstance(field, (Field, Struct, Map, Optional)):
    #    raise ValueError(f"Cannot decode into a non bare.Field type: {field}")
    if field._type == BareType.Struct:

M bare/test_encoder.py => bare/test_encoder.py +127 -19
@@ 1,14 1,16 @@
from .types import *
from .encoder import Struct, Map # TODO: fix import structure, structs should be somewhere else
import pytest

# TODO: fix import structure, structs should be somewhere else
from .encoder import Struct, Map, Array, _ValidatedMap, ValidationError, Optional, Union
from collections import OrderedDict
import pytest
import enum


class Nested(Struct):
    s = Str()
    # a = Array(Int(), 3)

class ExampleMap(Map):
    _key = Str()
    _value = Int()

class Example(Struct):



@@ 16,26 18,132 @@ class Example(Struct):
    teststr = Str()
    testuint = U8()
    n = Nested()
    m = ExampleMap()


def test_example_struct():
    ex = Example(testint=11, teststr="a test", m={'test': 1})
    assert hasattr(ex, 'testint')
    assert hasattr(ex, 'teststr')
    n = Nested(s="nested")
    ex = Example(testint=11, teststr="a test", m={"test": 1}, n=n)
    assert hasattr(ex, "testint")
    assert hasattr(ex, "teststr")
    assert hasattr(ex, "n")
    assert hasattr(ex.n, "s")
    assert ex.testint == 11
    assert ex.teststr == 'a test'
    assert ex.m == OrderedDict(test=1)
    ex2 = Example(testint=12, teststr='another test')
    assert ex.teststr == "a test"
    assert ex.n.s == "nested"
    ex2 = Example(testint=12, teststr="another test")
    assert ex2.testint == 12
    assert ex2.teststr == 'another test'
    assert ex2.teststr == "another test"
    # Check that the values in the original instance haven't been modified
    assert ex.testint == 11
    assert ex.teststr == 'a test'
    result = ex.pack()
    ex3 = Example.unpack(result)
    assert ex3.testint == ex.testint
    assert ex3.testint == ex.testint
    assert ex3.m == ex.m
    assert ex.teststr == "a test"


class ExampleMap(Map):
    _keytype = Str
    _valuetype = Str


class ExampleMapStruct(Struct):
    m = Map(Str, Int)
    m2 = Map(Int, Str)
    m3 = ExampleMap()


def test_map():
    ex = ExampleMapStruct()
    ex.m["test"] = 2
    ex.m2 = {0: "test"}
    ex.m3 = {"test": "test"}
    assert isinstance(ex.m, _ValidatedMap)
    assert isinstance(ex.m2, _ValidatedMap)
    assert isinstance(ex.m3, _ValidatedMap)
    assert ex.m2[0] == "test"
    assert ex.m["test"] == 2
    assert ex.m3["test"] == "test"
    with pytest.raises(ValidationError):
        ex.m2[0] = 0
    with pytest.raises(ValidationError):
        ex.m2 = {"test": "test"}
    with pytest.raises(ValidationError):
        ex.m3["3"] = 3
    map = Map(Str(), Str(), values={"test": "test"})
    assert map.value["test"] == "test"
    map2 = Map(Str(), Str())
    map2.value["another"] = "test"
    assert map2.value["another"] == "test"


class ArrayTest(Struct):
    a = Array(Int)
    n = Array(Nested, length=1)


def test_array():
    ex = ArrayTest()
    ex.a = [1, 2, 3]
    ex.n = [Nested(s="test")]
    assert ex.a == [1, 2, 3]
    with pytest.raises(ValidationError):
        ex.a = ["a", "b", "c"]
    Array(Int).validate([1, 2, 3])


class OptionalStruct(Struct):
    i = Int()
    s = Optional(Str)
    nested = Optional(Nested)


def test_optional():
    ex = OptionalStruct(i=1, s=None)
    assert ex.s is None
    assert ex.nested is None
    ex.s = "test"
    assert ex.s == "test"
    with pytest.raises(ValidationError):
        ex.s = 1
    ex.s = None
    assert ex.s is None

    ex.nested = Nested(s="test")
    assert ex.nested.s == "test"
    with pytest.raises(ValidationError):
        ex.nested = "test"


class ExampleUnion(Union):
    _members = (Str, Int)


class UnionTest(Struct):
    e = ExampleUnion()
    b = Union(members=(Str, Int))
    c = Union(members=(OptionalStruct,ArrayTest))

def test_union():
    ex = UnionTest(e=1, b="test", c=ArrayTest(a=[1], n=[Nested(s='s')])) # MUST specify values for union types when creating an object
    assert ex.e == 1
    ex.e = "1"
    assert ex.e == "1"
    with pytest.raises(ValidationError):
        ex.e = {"test": "test"}
    b = ex.pack()
    ex2 = UnionTest.unpack(b)
    assert ex.e == ex.e
    assert ex.b == ex.b
    assert ex.c.a == [1]
    assert ex.c.n[0].s == 's'
    assert ex.c.a == [1]

class EnumTest(enum.Enum):
    TEST = 0
    TEST2 = 1

class EnumTestStruct(Struct):
    e = Enum(EnumTest)

def test_enum():
    ex = EnumTestStruct(e=0)
    assert ex.e == 0
    with pytest.raises(ValidationError):
        ex.e = 100

M bare/types.py => bare/types.py +349 -9
@@ 1,28 1,368 @@
from .encoder import Field, BareType
from .encoder import (
    Field,
    BareType,
    _write_string,
    _write_varint,
    _read_string,
    _read_varint,
)
import typing
import struct
from enum import IntEnum

ValidationMessage = typing.Tuple[bool, str]


class Simple(Field):
    _fmt = None
    _bytesize = 0

    def _pack(self, fp: typing.BinaryIO, value=None):
        if value is None:
            value = self._value
        fp.write(struct.pack(self.__class__._fmt, value))

    @classmethod
    def _unpack(cls, fp: typing.BinaryIO):
        buf = fp.read(cls._bytesize)
        return cls(value=(struct.unpack(cls._fmt, buf)[0]))


class U8(Simple):

    _type = BareType.U8
    _default = 0
    _fmt = "<B"
    _bytesize = 1

    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, int):
            return False, f"type: {type(value)} must be <int>"
        if value < 0 or value > 0xFFFF:
            return (
                False,
                f"value: {value} is outside of valid range for this type: {self.__class__._type}",
            )
        return True, None


class U16(Simple):
    _type = BareType.U16
    _default = 0
    _fmt = "<H"
    _bytesize = 2

    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, int):
            return False, f"type: {type(value)} must be <int>"
        if value < 0 or value > 0xFFFFFFFF:
            return (
                False,
                f"value: {value} is outside of valid range for this type: {self.__class__._type}",
            )
        return True, None


class U32(Simple):
    _type = BareType.U32
    _default = 0
    _fmt = "<I"
    _bytesize = 4

    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, int):
            return False, f"type: {type(value)} must be <int>"
        if value < 0 or value > 0xFFFFFFFF:
            return (
                False,
                f"value: {value} is outside of valid range for this type: {self.__class__._type}",
            )
        return True, None


class U64(Simple):

    _type = BareType.U64
    _default = 0
    _fmt = "<Q"
    _bytesize = 8

    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, int):
            return False, f"type: {type(value)} must be <int>"
        if value < 0 or value > 0xFFFFFFFFFFFFFFFF:
            return (
                False,
                f"value: {value} is outside of valid range for this type: {self.__class__._type}",
            )
        return True, None


class I8(Simple):
    _type = BareType.I8
    _default = 0
    _fmt = "<b"
    _bytesize = 1

    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, int):
            return False, f"type: {type(value)} must be <int>"
        if value < -128 or value > 127:
            return (
                False,
                f"value: {value} is outside of valid range for this type: {self.__class__._type}",
            )
        return True, None


class I16(Simple):
    _type = BareType.I16
    _default = 0
    _fmt = "<h"
    _bytesize = 2

    @classmethod
    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, int):
            return False, f"type: {type(value)} must be <int>"
        if value < -32768 or value > 32767:
            return (
                False,
                f"value: {value} is outside of valid range for this type: {self.__class__._type}",
            )
        return True, None


class I32(Simple):
    _type = BareType.I32
    _default = 0
    _fmt = "<i"
    _bytesize = 4

    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, int):
            return False, f"type: {type(value)} must be <int>"
        if value < -2147483648 or value > 2147483647:
            return (
                False,
                f"value: {value} is outside of valid range for this type: {self.__class__._type}",
            )
        return True, None


class I64(Simple):
    _type = BareType.I64
    _default = 0
    _fmt = "<q"
    _bytesize = 8

    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, int):
            return False, f"type: {type(value)} must be <int>"
        if value < -9223372036854775808 or value > 9223372036854775807:
            return (
                False,
                f"value: {value} is outside of valid range for this type: {self.__class__._type}",
            )
        return True, None


class F32(Simple):
    _type = BareType.F32
    _default = 0.0
    _fmt = "<f"
    _bytesize = 4

    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, float):
            return False, f"type: {type(value)} must be <int>"
        return True, None


class F64(Simple):
    _type = BareType.F32
    _default = 0.0
    _fmt = "<d"
    _bytesize = 8

    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, float):
            return False, f"type: {type(value)} must be <int>"
        return True, None


class Bool(Simple):
    _type = BareType.Bool
    _default = False
    _fmt = "<?"
    _bytesize = 1

    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, bool):
            return False, f"type: {type(value)} must be <int>"
        return True, None


class Void(Field):
    _type = BareType.Void
    _default = None
    _fmt = None
    _bytesize = 0

    def _pack(self, fp: typing.BinaryIO, value=None):
        pass  # NO OP

    @classmethod
    def _unpack(cls, fp: typing.BinaryIO):
        return cls(value=None)

    def validate(self, value):
        if value is not None:
            return False, f"type: {type(value)} must be <None>"
        return True, None


class Int(Field):

    _type = BareType.INT
    _default = 0

    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, int):
            return False, f"type: {type(value)} must be <int>"
        return True, None

    def _pack(self, fp: typing.BinaryIO, value=None):
        if value is None:
            value = self._value
        _write_varint(fp, value, signed=True)

    @classmethod
    def _unpack(cls, fp: typing.BinaryIO) -> "Int":
        val = _read_varint(fp, signed=True)
        return cls(value=val)

class UInt(Field):

    _type = BareType.UINT
    _default = 0

    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, int):
            return False, f"type: {type(value)} must be <int>"
        if value < 0:
            return False, f"value: {value} is outside of valid range for this type: {self.__class__._type}",
        return True, None

    def _pack(self, fp: typing.BinaryIO, value=None):
        if value is None:
            value = self._value
        _write_varint(fp, value, signed=False)

    @classmethod
    def validate(cls, value):
        return isinstance(value, int) # TODO: check whether within min/max allowed by 64-bit precision
    def _unpack(cls, fp: typing.BinaryIO) -> "UInt":
        val = _read_varint(fp, signed=False)
        return cls(value=val)


class Str(Field):

    _type = BareType.String
    _default = ""

    def validate(self, value) -> ValidationMessage:
        if not isinstance(value, str):
            return False, f"type: {type(value)} must be <str>"
        return True, None

    def _pack(self, fp: typing.BinaryIO, value=None):
        if value is None:
            value = self._value
        _write_string(fp, value)

    @classmethod
    def validate(cls, value):
        return isinstance(value, str)
    def _unpack(cls, fp: typing.BinaryIO) -> "Str":
        val = _read_string(fp)
        return cls(value=val)

class U8(Field):

    _type = BareType.U8
    _default = 0
class Data(Field):
    _type = BareType.Data
    _default = bytes()

    def validate(self, value) -> ValidationMessage:
        try:
            value.decode()
            return True, None
        except (UnicodeDecodeError, AttributeError):
            return False, f"type: {type(value)} is not valid for Data type"

    def _pack(self, fp: typing.BinaryIO, value=None):
        if value is None:
            value = self._value
        length = len(value)
        if isinstance(value, str):
            value = value.encode("utf-8")
        _write_varint(fp, val=length, signed=False)
        fp.write(fp.write(struct.pack(f"<{len(value)}s", value)))

    @classmethod
    def _unpack(cls, fp: typing.BinaryIO) -> "Data":
        length = _read_varint(fp, signed=False)
        val = struct.unpack("<{length}s", fp)[0]
        return cls(value=val)


class DataFixed(Field):
    _type = BareType.DataFixed
    _length = 0
    _default = bytes(_length)

    def __init__(self, length=0, value=None):
        if length == 0 and self.__class__._length > 0:
            self._length = self.__class__._length
        else:
            self._length = length
        if value is not None:
            self._value = value
        else:
            self._value = self.__class__._default

    def validate(self, value) -> ValidationMessage:
        try:
            value.decode()
        except (UnicodeDecodeError, AttributeError):
            return False, f"type: {type(value)} is not valid for Data type"
        if len(value) != self._length:
            return (
                False,
                f"Length of data {len(value)} not equal to fixed length {self._length}",
            )

    def _pack(self, fp: typing.BinaryIO, value=None):
        if value is None:
            value = self._value
        fp.write(struct.pack(f"<{self._length}s", value))

    @classmethod
    def _unpack(cls, fp: typing.BinaryIO, length=None) -> "DataFixed":
        if length is None:
            length = cls._length
        val = struct.unpack(f"<{length}s", fp)[0]
        return cls(value=val)

class Enum(UInt):

    def __init__(self, enum, *args, **kwargs):
        self._enum = enum
        super().__init__(*args, **kwargs)

    def validate(self, value):
        return isinstance(value, int) and value <= 255
        if not isinstance(value, int):
            return False, f"type: {type(value)} is not valid for Enum, must be <int>"
        if value < 0:
            return False, f"value is not a valid value for Enum {self.__class__.__name__}"
        values = set(item.value for item in self._enum.__members__.values())
        if value not in values:
            return False, f"value {value} is not a valid Enum type for {self.__class__.__name__}"
        return True, None