tape: Implement option type in the dynamic system

This commit is contained in:
Sasha Koshka 2025-10-15 17:19:07 -04:00
parent 7bebc8c5eb
commit 77a4d7893f
3 changed files with 114 additions and 32 deletions

View File

@ -6,6 +6,10 @@ package tape
// TODO: add support for struct tags: `tape:"0000"`, tape:"0001"` so they can get
// transformed into tables with a defined schema
// TODO: support special behavior for options in structs: don't just write a
// zero value if the option is void, write no field at all. also consider doing
// this for maps, and maybe slices.
// TODO: test all of these smaller functions individually
// For an explanation as to why this package always treats LBA/SBA as strings,
@ -79,6 +83,12 @@ func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) {
return n, nil
}
// option
if isTypeOption(reflectValue.Type()) {
elemValue, _ := optionValue(reflectValue) // zero value for free
return EncodeAny(encoder, elemValue, tag)
}
// aggregates
reflectType := reflect.TypeOf(value)
switch reflectType.Kind() {
@ -310,6 +320,12 @@ func tagAny(reflectValue reflect.Value) (Tag, error) {
if reflectValue.CanConvert(reflect.TypeOf(dummyBuffer)) {
return bufferLenTag(reflectValue.Len()), nil
}
// option
if isTypeOption(reflectValue.Type()) {
elem, _ := optionValue(reflectValue) // zero value for free
return tagAny(elem)
}
// aggregates
reflectType := reflectValue.Type()
@ -336,9 +352,14 @@ func encodeAnySlice(encoder *Encoder, value any, tag Tag) (n int, err error) {
for index := 0; index < reflectValue.Len(); index += 1 {
itemTag, err := tagAny(reflectValue.Index(index))
if err != nil { return n, err }
if itemTag.Is(SBA) {
// SBA data in an LBA will always have the tag LBA:0,
// because 32 <= 256
continue
}
if itemTag.CN() > oneTag.CN() { oneTag = itemTag }
}
if oneTag.Is(SBA) { oneTag += 1 << 5 }
if oneTag.Is(SBA) { oneTag = LBA.WithCN(oneTag.CN()) }
nn, err = encoder.WriteUint8(uint8(oneTag))
n += nn; if err != nil { return n, err }
for index := 0; index < reflectValue.Len(); index += 1 {
@ -576,6 +597,21 @@ func isTypeAny(typ reflect.Type) bool {
return typ.Kind() == reflect.Interface && typ.NumMethod() == 0
}
// isTypeOption returns whether the given reflect.Type is a ucontainer.Option,
// and returns the element type if true.
func isTypeOption(typ reflect.Type) bool {
// TODO: change when needed
goutilPath := "git.tebibyte.media/sashakoshka/go-util"
return typ.Name() == "Option" && typ.PkgPath() == goutilPath + "/container"
}
// optionValue returns the value of an option. The value MUST be an option, or
// this function will panic.
func optionValue(value reflect.Value) (elem reflect.Value, ok bool) {
result := value.MethodByName("Value").Call([]reflect.Value { })
return result[0], result[1].Bool()
}
// peekSlice returns the element tag and dimension count of the OTA currently
// being decoded. It does not use up the decoder, it only peeks.
func peekSlice(decoder *Decoder, tag Tag) (Tag, int, error) {

View File

@ -3,39 +3,10 @@ package tape
import "bytes"
import "testing"
import "reflect"
import "git.tebibyte.media/sashakoshka/go-util/container"
import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil"
var samplePayloads = [][]byte {
/* int8 */ []byte { byte(LSI.WithCN(0)), 0x45 },
/* int16 */ []byte { byte(LSI.WithCN(1)), 0x45, 0x67 },
/* int32 */ []byte { byte(LSI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB },
/* int64 */ []byte { byte(LSI.WithCN(7)), 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23 },
/* uint5 */ []byte { byte(SI.WithCN(12)) },
/* uint8 */ []byte { byte(LI.WithCN(0)), 0x45 },
/* uint16 */ []byte { byte(LI.WithCN(1)), 0x45, 0x67 },
/* uint32 */ []byte { byte(LI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB },
/* uint64 */ []byte { byte(LI.WithCN(7)), 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23 },
/* bool */ []byte { byte(SI.WithCN(0)) },
/* bool */ []byte { byte(SI.WithCN(1)) },
/* string */ []byte { byte(SBA.WithCN(7)), 'p', 'u', 'p', 'e', 'v', 'e', 'r' },
/* []byte */ []byte { byte(SBA.WithCN(5)), 'b', 'l', 'a', 'r', 'g' },
/* []string */ []byte {
byte(OTA.WithCN(0)), 2, byte(LBA.WithCN(0)),
0x08, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23,
0x05, 0x11, 0x11, 0x11, 0x11, 0x11,
},
/* map[uint16] any */ []byte {
byte(KTV.WithCN(0)), 2,
0x02, 0x23, byte(LSI.WithCN(1)), 0x45, 0x67,
0x02, 0x24, byte(LI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB,
},
/* map[uint16] any */ []byte {
byte(KTV.WithCN(0)), 3,
0x00, 0x01, 0x63, 0x43, 0xF4, 0xC0, 0x00,
0x00, 0x02, 0x82, 'h', 'i',
0x00, 0x03, 0x21, 0x39, 0x92,
},
}
var samplePayloads [][]byte
var sampleValues = []any {
/* int8 */ int8(0x45),
@ -64,6 +35,64 @@ var sampleValues = []any {
0x0002: "hi",
0x0003: uint16(0x3992),
},
// IMPORTANT: ONLY ADD AT THE END!!!! DO NOT MOVE WHAT IS ALREADY HERE!
// IMPORTANT: ONLY ADD AT THE END!!!! DO NOT MOVE WHAT IS ALREADY HERE!
}
type sample struct {
t Tag
v any
s tu.Snake
}
var samples = []sample {
/* int8 */ sample { t: LSI.WithCN(0), s: tu.S(0x45) },
/* int16 */ sample { t: LSI.WithCN(1), s: tu.S(0x45, 0x67) },
/* int32 */ sample { t: LSI.WithCN(3), s: tu.S(0x45, 0x67, 0x89, 0xAB) },
/* int64 */ sample { t: LSI.WithCN(7), s: tu.S(0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23) },
/* uint5 */ sample { t: SI.WithCN(12), s: tu.S() },
/* uint8 */ sample { t: LI.WithCN(0), s: tu.S(0x45) },
/* uint16 */ sample { t: LI.WithCN(1), s: tu.S(0x45, 0x67) },
/* uint32 */ sample { t: LI.WithCN(3), s: tu.S(0x45, 0x67, 0x89, 0xAB) },
/* uint64 */ sample { t: LI.WithCN(7), s: tu.S(0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23) },
/* bool */ sample { t: SI.WithCN(0), s: tu.S() },
/* bool */ sample { t: SI.WithCN(1), s: tu.S() },
/* string */ sample { t: SBA.WithCN(7), s: tu.S('p', 'u', 'p', 'e', 'v', 'e', 'r') },
/* []byte */ sample { t: SBA.WithCN(5), s: tu.S('b', 'l', 'a', 'r', 'g') },
/* []string */ sample {
t: OTA.WithCN(0),
s: tu.S(2, byte(LBA.WithCN(0)),
0x08, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23,
0x05, 0x11, 0x11, 0x11, 0x11, 0x11),
},
/* map[uint16] any */ sample {
t: KTV.WithCN(0),
s: tu.S(2).AddVar(
[]byte { 0x02, 0x23, byte(LSI.WithCN(1)), 0x45, 0x67 },
[]byte { 0x02, 0x24, byte(LI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB }),
},
/* map[uint16] any */ sample {
t: KTV.WithCN(0),
s: tu.S(3).AddVar(
[]byte { 0x00, 0x01, 0x63, 0x43, 0xF4, 0xC0, 0x00 },
[]byte { 0x00, 0x02, 0x82, 'h', 'i' },
[]byte { 0x00, 0x03, 0x21, 0x39, 0x92 }),
},
}
var sampleOptionValues []any
func init() {
sampleOptionValues = make([]any, len(sampleValues))
for index, value := range sampleValues {
sampleOptionValues[index] = ucontainer.O(value)
samples[index].v = value
}
samplePayloads = make([][]byte, len(samples))
for index, sample := range samples {
item := append([]byte { byte(sample.t) }, sample.s.Flatten()...)
samplePayloads[index] = item
}
}
type userDefinedInteger int16
@ -271,6 +300,20 @@ func TestEncodeDecodeAnyDestination(test *testing.T) {
}
}
func TestEncodeOption(test *testing.T) {
for _, sample := range samples {
snake := sample.s
tag := sample.t
value := sample.v
if _, ok := value.(bool); tag.Is(SI) && !ok {
// we will never encode an SI unless its a bool
continue
}
err := testEncodeAny(test, value, tag, snake)
if err != nil { test.Fatal(err) }
}
}
func TestPeekSlice(test *testing.T) {
buffer := bytes.NewBuffer([]byte {
2, byte(OTA.WithCN(3)),

View File

@ -32,7 +32,9 @@ func testEncodeAny(test *testing.T, value any, correctTag Tag, correctBytes tu.S
test.Log("n: ", n)
test.Log("tag: ", tag)
test.Log("got: ", tu.HexBytes(bytes))
test.Log(" : ", tu.HexChars(bytes))
test.Log("correct:", correctBytes)
test.Log(" :", correctBytes.CharsString())
if tag != correctTag {
return fmt.Errorf("tag not equal: %v != %v", tag, correctTag)
}
@ -56,6 +58,7 @@ func testEncodeDecodeAny(test *testing.T, value, correctValue any) error {
test.Log("n: ", n)
test.Log("tag:", tag)
test.Log("got:", tu.HexBytes(bytes))
test.Log(" :", tu.HexChars(bytes))
test.Log("decoding...", tag)
if n != len(bytes) {
return fmt.Errorf("n not equal: %d != %d", n, len(bytes))