fuller implementation of chunk and unmarshal
This commit is contained in:
parent
3004475000
commit
9514f9c374
|
@ -7,7 +7,7 @@ func Map[T, V any](c Vector[T], f func(T) V) Vector[V] {
|
|||
out[index] = f(c[index])
|
||||
}
|
||||
|
||||
return From(out)
|
||||
return From(out...)
|
||||
}
|
||||
|
||||
func Filter[T any](v Vector[T], f func(T) bool) Vector[T] {
|
||||
|
@ -27,7 +27,7 @@ func Take[T any](v Vector[T], howMany int) Vector[T] {
|
|||
if len(v) < howMany {
|
||||
howMany = len(v)
|
||||
}
|
||||
return From(v[:howMany-1])
|
||||
return From(v[:howMany-1]...)
|
||||
}
|
||||
|
||||
func Any[T any](v Vector[T], f func(T) bool) bool {
|
||||
|
@ -39,6 +39,15 @@ func Any[T any](v Vector[T], f func(T) bool) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func AnyOf[T comparable](v Vector[T], t T) bool {
|
||||
for _, vT := range v {
|
||||
if vT == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type numeric interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ func WithCap[T any](capacity int) Vector[T] {
|
|||
return make(Vector[T], 0, capacity)
|
||||
}
|
||||
|
||||
func From[T any](v []T) Vector[T] {
|
||||
func From[T any](v ...T) Vector[T] {
|
||||
return Vector[T](v)
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,7 @@ func (v Vector[T]) Append(t ...T) Vector[T] {
|
|||
}
|
||||
|
||||
func (v Vector[T]) Remove(index int) Vector[T] {
|
||||
return From(append(v[:index], v[index+1:]...))
|
||||
return From(append(v[:index], v[index+1:]...)...)
|
||||
}
|
||||
|
||||
// Faster remove function that can be used on vectors where ordering doesn't matter
|
||||
|
@ -110,12 +110,12 @@ func (v Vector[T]) Sub(start, end int) Vector[T] {
|
|||
v.panicIndex(end)
|
||||
}
|
||||
if start < 0 {
|
||||
return From(v[:end])
|
||||
return From(v[:end]...)
|
||||
}
|
||||
if end < 0 {
|
||||
return From(v[start:])
|
||||
return From(v[start:]...)
|
||||
}
|
||||
return From(v[start:end])
|
||||
return From(v[start:end]...)
|
||||
}
|
||||
|
||||
func (v Vector[T]) Filter(f func(T) bool) Vector[T] {
|
||||
|
|
|
@ -59,18 +59,18 @@ func TestVector(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"append": func(that *require.Assertions) {
|
||||
v := coll.From([]int{1, 2, 3}).Append(4, 5, 6)
|
||||
v := coll.From([]int{1, 2, 3}...).Append(4, 5, 6)
|
||||
that.Equal(6, len(v))
|
||||
that.EqualValues([]int{1, 2, 3, 4, 5, 6}, v)
|
||||
},
|
||||
"getsoft": func(that *require.Assertions) {
|
||||
v := coll.From([]int{1, 2, 3})
|
||||
v := coll.From([]int{1, 2, 3}...)
|
||||
that.Equal(2, v.GetSoft(1))
|
||||
that.Zero(v.GetSoft(-1000))
|
||||
that.Zero(v.GetSoft(1000))
|
||||
},
|
||||
"get": func(that *require.Assertions) {
|
||||
v := coll.From([]int{1, 2, 3})
|
||||
v := coll.From([]int{1, 2, 3}...)
|
||||
that.Equal(2, v.Get(1))
|
||||
that.Panics(func() {
|
||||
v.Get(-1000)
|
||||
|
@ -80,7 +80,7 @@ func TestVector(t *testing.T) {
|
|||
})
|
||||
},
|
||||
"clone": func(that *require.Assertions) {
|
||||
v := coll.From([]int{1, 2, 3})
|
||||
v := coll.From([]int{1, 2, 3}...)
|
||||
|
||||
v1 := v.Clone().Set(1, 1)
|
||||
that.Equal(3, len(v1))
|
||||
|
@ -88,7 +88,7 @@ func TestVector(t *testing.T) {
|
|||
that.EqualValues([]int{1, 2, 3}, v)
|
||||
},
|
||||
"set": func(that *require.Assertions) {
|
||||
v := coll.From([]int{1, 2, 3})
|
||||
v := coll.From([]int{1, 2, 3}...)
|
||||
|
||||
v1 := v.Clone().Set(1, 1)
|
||||
that.Equal(3, len(v1))
|
||||
|
@ -110,7 +110,7 @@ func TestVector(t *testing.T) {
|
|||
})
|
||||
},
|
||||
"sub": func(that *require.Assertions) {
|
||||
v := coll.From([]int{0, 1, 2, 3, 4, 5, 6})
|
||||
v := coll.From([]int{0, 1, 2, 3, 4, 5, 6}...)
|
||||
|
||||
v1 := v.Clone().Sub(2, 4)
|
||||
that.Equal(2, len(v1))
|
||||
|
@ -139,13 +139,13 @@ func TestVector(t *testing.T) {
|
|||
})
|
||||
},
|
||||
"filter": func(that *require.Assertions) {
|
||||
v := coll.From([]int{1, 2, 3, 4, 5, 6})
|
||||
v := coll.From([]int{1, 2, 3, 4, 5, 6}...)
|
||||
v = v.Filter(func(i int) bool { return i%2 == 0 })
|
||||
that.Len(v, 3)
|
||||
},
|
||||
"any": func(that *require.Assertions) {
|
||||
that.True(coll.From([]int{1, 2, 3, 4, 5, 6}).Any(func(i int) bool { return i == 3 }))
|
||||
that.False(coll.From([]int{1, 2, 3, 4, 5, 6}).Any(func(i int) bool { return i == 666 }))
|
||||
that.True(coll.From([]int{1, 2, 3, 4, 5, 6}...).Any(func(i int) bool { return i == 3 }))
|
||||
that.False(coll.From([]int{1, 2, 3, 4, 5, 6}...).Any(func(i int) bool { return i == 666 }))
|
||||
},
|
||||
"take": func(that *require.Assertions) {
|
||||
that.EqualValues([]int{1, 2, 3}, coll.From([]int{1, 2, 3, 4, 5, 6}).Take(3))
|
||||
|
|
|
@ -3,16 +3,22 @@ package chunk
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"sectorinf.com/emilis/flabk/pkg/coll"
|
||||
)
|
||||
|
||||
const (
|
||||
escapeChar = '\\'
|
||||
unicodeEscape = 'u'
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoMatch = errors.New("no matching")
|
||||
ErrMapMemberNotString = errors.New("map member not a string")
|
||||
ErrUnexpectedSymbol = errors.New("unexpected symbol")
|
||||
ErrMissingValue = errors.New("missing value")
|
||||
ErrEscapeAtEnd = errors.New("escape character at the end of value")
|
||||
ErrIncompleteEscape = errors.New("incomplete unicode escape sequence")
|
||||
)
|
||||
|
||||
type Chunk struct {
|
||||
|
@ -24,72 +30,120 @@ type Chunk struct {
|
|||
globRight int
|
||||
}
|
||||
|
||||
func (c Chunk) LeftByte() byte {
|
||||
return c.vector[c.posLeft]
|
||||
}
|
||||
|
||||
func (c Chunk) Null() bool {
|
||||
return (c.posRight+1)-c.posLeft >= 4 && c.vector[c.posLeft] == 'n' && c.vector[c.posLeft+1] == 'u' && c.vector[c.posLeft+2] == 'l' && c.vector[c.posLeft+3] == 'l'
|
||||
}
|
||||
|
||||
func (c Chunk) LeftPosf(format string, args ...any) error {
|
||||
return fmt.Errorf(fmt.Sprintf("[%d] %s", c.globLeft, format), args...)
|
||||
}
|
||||
|
||||
// Match finds the matching closer to the symbol at the left position and sets the
|
||||
// right position to this index
|
||||
func (c Chunk) Match() (Chunk, error) {
|
||||
s, ok := matchers[c.vector[c.posLeft]]
|
||||
start := c.vector[c.posLeft]
|
||||
matcher, ok := matchers[start]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Match called on %c with no matcher defined", c.vector[c.posLeft]))
|
||||
}
|
||||
if s.StartFromRight {
|
||||
for c.posRight > 0 {
|
||||
if c.vector[c.posRight] == s.MatchByte {
|
||||
return c, nil
|
||||
|
||||
for index := c.posLeft + 1; index < len(c.vector); index++ {
|
||||
if start != '"' && c.vector[index] == start {
|
||||
sub, err := c.Child(index, len(c.vector)).Match()
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("[%d] child %w", c.globLeft+(c.posLeft-index), err)
|
||||
}
|
||||
c.posRight--
|
||||
c.globRight--
|
||||
index += sub.posRight
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
for index := c.posLeft + 1; index < len(c.vector); index++ {
|
||||
if c.vector[index] == s.MatchByte {
|
||||
c.posRight = index
|
||||
c.globRight += (c.posLeft + 1) - index
|
||||
return c, nil
|
||||
if c.vector[index] == escapeChar {
|
||||
if index+1 == len(c.vector) {
|
||||
return c, fmt.Errorf("[%d] %w", c.globLeft+(c.posLeft-index), ErrEscapeAtEnd)
|
||||
}
|
||||
if c.vector[index+1] == unicodeEscape {
|
||||
if index+6 >= len(c.vector) {
|
||||
return c, fmt.Errorf("[%d] %w", c.globLeft+(c.posLeft+index), ErrIncompleteEscape)
|
||||
}
|
||||
index += 5
|
||||
continue
|
||||
}
|
||||
index++
|
||||
continue
|
||||
}
|
||||
if c.vector[index] == matcher {
|
||||
c.globRight -= c.posRight - index
|
||||
c.posRight = index
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
return c, fmt.Errorf("%w %c", ErrNoMatch, c.vector[c.posLeft])
|
||||
return c, c.LeftPosf("%w %c", ErrNoMatch, c.vector[c.posLeft])
|
||||
}
|
||||
|
||||
func (c Chunk) Copy() Chunk {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Chunk) Left() byte {
|
||||
return c.vector[c.posLeft]
|
||||
}
|
||||
|
||||
// Sub returns an inclusive subchunk of [left;right]
|
||||
func (c Chunk) Sub() Chunk {
|
||||
return New(c.vector[c.posLeft : c.posRight+1])
|
||||
c = c.Child(c.posLeft, c.posRight+1)
|
||||
// To be inclusive we incremented posRight above, so
|
||||
// to restore the original position, we must decrement
|
||||
// it here before returning. Only if we have room to decrement it
|
||||
if c.posRight > 0 {
|
||||
c.posRight--
|
||||
c.globRight--
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// CookieCutter returns a subchunk of (left;right)
|
||||
func (c Chunk) CookieCutter() Chunk {
|
||||
return New(c.vector[c.posLeft+1 : c.posRight])
|
||||
return c.Child(c.posLeft+1, c.posRight)
|
||||
}
|
||||
|
||||
func spaceAtIndex(v coll.Vector[byte], index int) bool {
|
||||
return v[index] == 0x9 || v[index] == 0xa || v[index] == 0xd || v[index] == 0x20
|
||||
}
|
||||
|
||||
func (c Chunk) AtSpace() bool {
|
||||
r, _ := utf8.DecodeRune(c.vector[c.posLeft:])
|
||||
return unicode.IsSpace(r)
|
||||
return spaceAtIndex(c.vector, c.posLeft)
|
||||
}
|
||||
|
||||
func (c Chunk) Seek() Chunk {
|
||||
for c.posLeft < len(c.vector) && c.AtSpace() {
|
||||
c.globLeft++
|
||||
c.posLeft++
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Step increments the left position by 1 if b
|
||||
// is the byte at the current left position
|
||||
func (c Chunk) StepIf(b byte) Chunk {
|
||||
if c.vector[c.posLeft] != b {
|
||||
return c
|
||||
}
|
||||
c.posLeft++
|
||||
c.globLeft++
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Chunk) ValueEnd() (Chunk, error) {
|
||||
switch c.vector[c.posLeft] {
|
||||
case '"', '{', '[':
|
||||
return c.Match()
|
||||
default:
|
||||
for index := c.posLeft; index <= c.posRight; index++ {
|
||||
if c.vector[index] == ',' {
|
||||
return New(c.vector[c.posLeft:index]), nil
|
||||
if c.vector[index] == ',' || spaceAtIndex(c.vector, index) {
|
||||
if index == c.posLeft {
|
||||
return c, c.LeftPosf("%w", ErrMissingValue)
|
||||
}
|
||||
return c.Child(c.posLeft, index), nil
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
|
@ -100,6 +154,7 @@ func (c Chunk) ValueEnd() (Chunk, error) {
|
|||
func (c Chunk) Skip() Chunk {
|
||||
if c.posLeft+1 < len(c.vector) {
|
||||
c.posLeft++
|
||||
c.globLeft++
|
||||
}
|
||||
return c.Seek()
|
||||
}
|
||||
|
@ -110,18 +165,10 @@ type MatchRule struct {
|
|||
}
|
||||
|
||||
var (
|
||||
matchers = map[byte]MatchRule{
|
||||
'{': {
|
||||
MatchByte: '}',
|
||||
StartFromRight: true,
|
||||
},
|
||||
'[': {
|
||||
MatchByte: ']',
|
||||
StartFromRight: true,
|
||||
},
|
||||
'"': {
|
||||
MatchByte: '"',
|
||||
},
|
||||
matchers = map[byte]byte{
|
||||
'{': '}',
|
||||
'[': ']',
|
||||
'"': '"',
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -131,9 +178,10 @@ func New(v []byte) Chunk {
|
|||
posRight = 0
|
||||
}
|
||||
return Chunk{
|
||||
vector: coll.Vector[byte](v),
|
||||
posLeft: 0,
|
||||
posRight: posRight,
|
||||
vector: coll.Vector[byte](v),
|
||||
posLeft: 0,
|
||||
posRight: posRight,
|
||||
globRight: posRight,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,31 +190,14 @@ func (c Chunk) Child(left, right int) Chunk {
|
|||
return Chunk{
|
||||
vector: sub,
|
||||
posLeft: 0,
|
||||
posRight: len(sub),
|
||||
posRight: len(sub) - 1,
|
||||
globLeft: (c.globLeft - c.posLeft) + left,
|
||||
globRight: (c.globRight - c.posRight) + right,
|
||||
globRight: (c.globRight - c.posRight) + (right - 1),
|
||||
}
|
||||
}
|
||||
|
||||
type ParseFunc[T any] func(T) (T, error)
|
||||
|
||||
func Parse[T any](c Chunk) ParseFunc[T] {
|
||||
switch c.vector[c.posLeft] {
|
||||
case '{':
|
||||
return ParseMap[T](c.CookieCutter())
|
||||
default:
|
||||
panic("not implemented")
|
||||
}
|
||||
// return can be:
|
||||
// * {}
|
||||
// * []
|
||||
// * ""
|
||||
// * 123
|
||||
// * true
|
||||
// * false
|
||||
// * null
|
||||
}
|
||||
|
||||
type Row struct {
|
||||
Name string
|
||||
Value Chunk
|
||||
|
@ -177,47 +208,47 @@ func (c Chunk) String() string {
|
|||
}
|
||||
|
||||
func (c Chunk) Row() (Row, error) {
|
||||
c = c.Seek()
|
||||
if c.vector[c.posLeft] != '"' {
|
||||
return Row{}, fmt.Errorf("%w: %c", ErrMapMemberNotString, c.vector[c.posLeft])
|
||||
return Row{}, c.LeftPosf("%w: %c", ErrMapMemberNotString, c.vector[c.posLeft])
|
||||
}
|
||||
name, err := c.Match()
|
||||
if err != nil {
|
||||
return Row{}, fmt.Errorf("match: %w", err)
|
||||
return Row{}, c.LeftPosf("match: %w", err)
|
||||
}
|
||||
postName := c.Copy()
|
||||
postName.posLeft = name.posRight
|
||||
postName.globLeft = name.globRight
|
||||
postName = postName.Skip()
|
||||
// Next we must get a :
|
||||
if postName.vector[postName.posLeft] != ':' {
|
||||
return Row{}, fmt.Errorf("%w '%c', expected ':'", ErrUnexpectedSymbol, postName.vector[postName.posLeft])
|
||||
return Row{}, postName.LeftPosf("%w '%c', expected ':'", ErrUnexpectedSymbol, postName.vector[postName.posLeft])
|
||||
}
|
||||
value, err := postName.Skip().ValueEnd()
|
||||
if err != nil {
|
||||
return Row{}, fmt.Errorf("value: %w", err)
|
||||
return Row{}, postName.LeftPosf("value: %w", err)
|
||||
}
|
||||
|
||||
return Row{
|
||||
Name: name.String(),
|
||||
Name: name.String()[1 : name.posRight-name.posLeft],
|
||||
Value: value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// After returns the chunk with its left position, if possible,
|
||||
// right after the global right position of v
|
||||
func (c Chunk) After(v Chunk) Chunk {
|
||||
c.posLeft = v.posLeft
|
||||
// Add two, as one is for dealing with right side being exclusive
|
||||
// in slice indexes, and another one to go on to the next
|
||||
offset := (v.globRight - c.globLeft) + 1
|
||||
// Then, make sure we don't go too far
|
||||
if c.posLeft+offset >= len(c.vector) {
|
||||
offset--
|
||||
}
|
||||
c.posLeft += offset
|
||||
c.globLeft += offset
|
||||
return c
|
||||
}
|
||||
|
||||
func ParseMap[T any](c Chunk) ParseFunc[T] {
|
||||
return func(t T) (T, error) {
|
||||
// mapper := parse.GetMap(t)
|
||||
// for {
|
||||
// row, err := c.Row()
|
||||
// if err != nil {
|
||||
// return t, err
|
||||
// }
|
||||
|
||||
// }
|
||||
panic("todo")
|
||||
}
|
||||
func (c Chunk) EOF() bool {
|
||||
return c.posLeft >= len(c.vector)-1
|
||||
}
|
||||
|
|
|
@ -1,37 +1,108 @@
|
|||
package chunk_test
|
||||
package chunk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"sectorinf.com/emilis/flabk/pkg/ld/internal/parse/chunk"
|
||||
"sectorinf.com/emilis/flabk/pkg/coll"
|
||||
)
|
||||
|
||||
func TestSpace(t *testing.T) {
|
||||
tests := []byte{0x9, 0xa, 0xd, 0x20}
|
||||
|
||||
that := require.New(t)
|
||||
ch := chunk.New([]byte(" hello world"))
|
||||
that.True(ch.AtSpace())
|
||||
for _, test := range tests {
|
||||
b := []byte("xhello world")
|
||||
b[0] = test
|
||||
that.True(New(b).AtSpace())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMap(t *testing.T) {
|
||||
type Hello struct {
|
||||
Hello string
|
||||
func TestAlign(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
c func() Chunk
|
||||
v func(Chunk) Chunk
|
||||
}{
|
||||
"same-size": {
|
||||
c: func() Chunk {
|
||||
c := New(coll.Vector[byte](`{"heres some test data": "hi mom"}`))
|
||||
c.posLeft = 15
|
||||
c.globLeft = 15
|
||||
return c
|
||||
},
|
||||
v: func(c Chunk) Chunk {
|
||||
c.posLeft = 20
|
||||
c.globLeft = 20
|
||||
c.globRight = 22
|
||||
c.posRight = 22
|
||||
return c
|
||||
},
|
||||
},
|
||||
"cookie-cutter": {
|
||||
c: func() Chunk {
|
||||
c := New(coll.Vector[byte](`{"heres some test data": "hi mom"}`))
|
||||
c.posLeft = 15
|
||||
c.globLeft = 15
|
||||
return c
|
||||
},
|
||||
v: func(c Chunk) Chunk {
|
||||
return c.CookieCutter().CookieCutter()
|
||||
},
|
||||
},
|
||||
"no-change": {
|
||||
c: func() Chunk {
|
||||
c := New(coll.Vector[byte](`{"heres some test data": "hi mom"}`))
|
||||
c.posLeft = 15
|
||||
c.globLeft = 15
|
||||
return c
|
||||
},
|
||||
v: func(c Chunk) Chunk {
|
||||
c.posRight = 15
|
||||
c.globRight = 15
|
||||
return c.Sub()
|
||||
},
|
||||
},
|
||||
"contains": {
|
||||
c: func() Chunk {
|
||||
return New(coll.Vector[byte](`0123456789`))
|
||||
},
|
||||
v: func(c Chunk) Chunk {
|
||||
return c.Child(5, 8)
|
||||
},
|
||||
},
|
||||
}
|
||||
ch := chunk.New([]byte(`"Hello": "world"`))
|
||||
h, err := chunk.ParseMap[Hello](ch)(Hello{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
for name, test := range tests {
|
||||
test := test
|
||||
t.Run(name, func(tt *testing.T) {
|
||||
that := require.New(tt)
|
||||
c := test.c()
|
||||
v := test.v(c)
|
||||
out := c.After(v)
|
||||
that.Equal(v.globRight+1, out.globLeft)
|
||||
that.Equal(string(c.vector[v.globRight+1]), string(out.LeftByte()))
|
||||
})
|
||||
}
|
||||
fmt.Println(h)
|
||||
}
|
||||
|
||||
func TestChild(t *testing.T) {
|
||||
type Hello struct {
|
||||
Hello string
|
||||
}
|
||||
ch := chunk.New([]byte(`"Hello": "world"`))
|
||||
child := ch.Child(5, 10)
|
||||
|
||||
fmt.Println(child)
|
||||
func TestRow(t *testing.T) {
|
||||
c := New(coll.Vector[byte](`{
|
||||
"heres some test data": "hi mom",
|
||||
"another field": "get it right",
|
||||
"integer": -12345
|
||||
}`))
|
||||
that := require.New(t)
|
||||
row, err := c.CookieCutter().Seek().Row()
|
||||
that.NoError(err)
|
||||
that.Equal("heres some test data", row.Name)
|
||||
that.Equal(`"hi mom"`, row.Value.String())
|
||||
row, err = c.After(row.Value).StepIf(',').Seek().Row()
|
||||
that.NoError(err)
|
||||
that.Equal("another field", row.Name)
|
||||
that.Equal(`"get it right"`, row.Value.String())
|
||||
row, err = c.After(row.Value).StepIf(',').Seek().Row()
|
||||
that.NoError(err)
|
||||
that.Equal("integer", row.Name)
|
||||
that.Equal("-12345", row.Value.String())
|
||||
that.True(c.After(row.Value).Seek().EOF())
|
||||
}
|
||||
|
|
|
@ -1,54 +1,200 @@
|
|||
package parse
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sectorinf.com/emilis/flabk/pkg/coll"
|
||||
"sectorinf.com/emilis/flabk/pkg/ld/internal/consts"
|
||||
"sectorinf.com/emilis/flabk/pkg/ld/internal/parse/chunk"
|
||||
)
|
||||
|
||||
type LazyMapFunc func(name string, value string)
|
||||
const (
|
||||
memberSeparator = ','
|
||||
)
|
||||
|
||||
func GetMap(v any) LazyMapFunc {
|
||||
val := reflect.ValueOf(v)
|
||||
// typ := reflect.TypeOf(v)
|
||||
switch val.Kind() {
|
||||
case reflect.Map:
|
||||
return func(name string, value string) {
|
||||
// val.SetMapIndex(reflect.ValueOf(name), value)
|
||||
}
|
||||
case reflect.Struct:
|
||||
// fields := GetStructFields(val, typ)
|
||||
return func(name, value string) {
|
||||
// val, ok := fields[name]
|
||||
// if ok {
|
||||
// val.Set(value)
|
||||
// }
|
||||
}
|
||||
default:
|
||||
panic("wrong")
|
||||
var (
|
||||
ErrNotArray = errors.New("value is not an array")
|
||||
ErrNotMap = errors.New("value is not a map")
|
||||
)
|
||||
|
||||
var (
|
||||
boolVals = map[string]bool{
|
||||
"true": true,
|
||||
"false": false,
|
||||
}
|
||||
determineMap = func() map[byte]coll.Vector[reflect.Kind] {
|
||||
m := map[byte]coll.Vector[reflect.Kind]{
|
||||
'"': coll.From(reflect.String),
|
||||
'[': coll.From(reflect.Array, reflect.Slice),
|
||||
'{': coll.From(reflect.Map, reflect.Struct),
|
||||
'-': coll.From(reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64),
|
||||
't': coll.From(reflect.Bool),
|
||||
'f': coll.From(reflect.Bool),
|
||||
}
|
||||
numbers := coll.From(
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint64,
|
||||
reflect.Uint32,
|
||||
reflect.Uint16,
|
||||
reflect.Uint8,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
)
|
||||
for index := byte('0'); index <= '9'; index++ {
|
||||
m[index] = numbers
|
||||
}
|
||||
|
||||
return m
|
||||
}()
|
||||
sizes = map[reflect.Kind]int{
|
||||
// Int and Uint values stolen from the [math] package, as the intSize
|
||||
// constant is unexported
|
||||
reflect.Int: 32 << (^uint(0) >> 63), // 32 or 64
|
||||
reflect.Uint: 32 << (^uint(0) >> 63), // 32 or 64
|
||||
reflect.Int64: 64,
|
||||
reflect.Int32: 32,
|
||||
reflect.Int16: 16,
|
||||
reflect.Int8: 8,
|
||||
reflect.Uint64: 64,
|
||||
reflect.Uint32: 32,
|
||||
reflect.Uint16: 16,
|
||||
reflect.Uint8: 8,
|
||||
reflect.Float32: 32,
|
||||
reflect.Float64: 64,
|
||||
}
|
||||
)
|
||||
|
||||
func SetMap(val reflect.Value, typ reflect.Type, c chunk.Chunk) error {
|
||||
if c.LeftByte() != '{' {
|
||||
return c.LeftPosf("%w", ErrNotMap)
|
||||
}
|
||||
fields := GetStructFields(val, typ)
|
||||
c = c.CookieCutter().Seek()
|
||||
for !c.EOF() {
|
||||
row, err := c.Row()
|
||||
if err != nil {
|
||||
return fmt.Errorf("row: %w", err)
|
||||
}
|
||||
field, ok := fields[row.Name]
|
||||
if ok {
|
||||
if err := SetValue(field, field.Type(), row.Value); err != nil {
|
||||
return fmt.Errorf("set: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Shift c
|
||||
c = c.After(row.Value).StepIf(memberSeparator).Seek()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseAsValue(v string, valType reflect.Type) (any, error) {
|
||||
// Might not be necessary to trim
|
||||
v = strings.TrimSpace(v)
|
||||
switch valType.Kind() {
|
||||
case reflect.String:
|
||||
return v[1 : len(v)-1], nil
|
||||
case reflect.Bool:
|
||||
b, err := strconv.ParseBool(strings.ToLower(v))
|
||||
if err != nil {
|
||||
fmt.Errorf("boolean: %w", err)
|
||||
}
|
||||
return b, nil
|
||||
case reflect.Struct, reflect.Map:
|
||||
panic("todo")
|
||||
default:
|
||||
panic("todo")
|
||||
func SetArray(val reflect.Value, typ reflect.Type, c chunk.Chunk) error {
|
||||
if c.LeftByte() != '[' {
|
||||
return c.LeftPosf("%w", ErrNotArray)
|
||||
}
|
||||
|
||||
c = c.CookieCutter().Seek()
|
||||
elems := coll.New[chunk.Chunk]()
|
||||
for !c.EOF() {
|
||||
element, err := c.ValueEnd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting array elem: %w", err)
|
||||
}
|
||||
elems = elems.Push(element)
|
||||
c = c.After(element).StepIf(memberSeparator).Seek()
|
||||
}
|
||||
elementType := typ.Elem()
|
||||
arrayType := reflect.ArrayOf(len(elems), elementType)
|
||||
array := reflect.New(arrayType).Elem()
|
||||
for index, elem := range elems {
|
||||
if err := SetValue(array.Index(index), elementType, elem); err != nil {
|
||||
return fmt.Errorf("%w at array index [%d]", err, index)
|
||||
}
|
||||
}
|
||||
if val.Kind() == reflect.Slice {
|
||||
array = array.Slice(0, len(elems))
|
||||
}
|
||||
val.Set(array)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Determines what [reflect.Kind] of object the given [chunk.Chunk] is
|
||||
func Determine(c chunk.Chunk) coll.Vector[reflect.Kind] {
|
||||
k, ok := determineMap[c.LeftByte()]
|
||||
if !ok {
|
||||
return coll.From(reflect.Invalid)
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
func SetValue(val reflect.Value, typ reflect.Type, c chunk.Chunk) error {
|
||||
if c.Null() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if val.Kind() == reflect.Pointer {
|
||||
if val.IsNil() {
|
||||
val.Set(reflect.New(typ.Elem()))
|
||||
}
|
||||
return SetValue(val.Elem(), typ.Elem(), c)
|
||||
}
|
||||
|
||||
kinds := Determine(c)
|
||||
if !coll.AnyOf(kinds, val.Kind()) {
|
||||
return fmt.Errorf("kind %s not found in appropriate list %s for value [%s]", val.Kind(), kinds, c)
|
||||
}
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.Map, reflect.Struct:
|
||||
return SetMap(val, typ, c)
|
||||
case reflect.String:
|
||||
val.SetString(Escape(c.CookieCutter()))
|
||||
case reflect.Bool:
|
||||
b, ok := boolVals[c.String()]
|
||||
if !ok {
|
||||
return fmt.Errorf("value [%s] is not a boolean", c.String())
|
||||
}
|
||||
val.SetBool(b)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
i, err := strconv.ParseInt(c.String(), 10, sizes[val.Kind()])
|
||||
if err != nil {
|
||||
return fmt.Errorf("value [%s] is not an %s", c.String(), val.Kind().String())
|
||||
}
|
||||
val.SetInt(i)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
u, err := strconv.ParseUint(c.String(), 10, sizes[val.Kind()])
|
||||
if err != nil {
|
||||
return fmt.Errorf("value [%s] is not an %s", c.String(), val.Kind().String())
|
||||
}
|
||||
val.SetUint(u)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
f, err := strconv.ParseFloat(c.String(), sizes[val.Kind()])
|
||||
if err != nil {
|
||||
return fmt.Errorf("value [%s] is not an %s", c.String(), val.Kind().String())
|
||||
}
|
||||
val.SetFloat(f)
|
||||
case reflect.Array, reflect.Slice:
|
||||
return SetArray(val, typ, c)
|
||||
default:
|
||||
fmt.Println(val.Kind().String())
|
||||
panic("unsupported")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Escape(v chunk.Chunk) string {
|
||||
// TODO: string escaping
|
||||
return v.String()
|
||||
}
|
||||
|
||||
func GetStructFields(val reflect.Value, typ reflect.Type) map[string]reflect.Value {
|
||||
|
|
|
@ -1 +1,17 @@
|
|||
package ld
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"sectorinf.com/emilis/flabk/pkg/ld/internal/parse"
|
||||
"sectorinf.com/emilis/flabk/pkg/ld/internal/parse/chunk"
|
||||
)
|
||||
|
||||
func Unmarshal[T any](v []byte) (T, error) {
|
||||
ptr := new(T)
|
||||
if err := parse.SetValue(reflect.ValueOf(ptr), reflect.TypeOf(ptr), chunk.New(v)); err != nil {
|
||||
return *ptr, fmt.Errorf("unmarshal: %w", err)
|
||||
}
|
||||
return *ptr, nil
|
||||
}
|
||||
|
|
|
@ -1,192 +1,192 @@
|
|||
package ld_test
|
||||
|
||||
// import (
|
||||
// "encoding/json"
|
||||
// "fmt"
|
||||
// "math"
|
||||
// "testing"
|
||||
// "time"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
// "sectorinf.com/emilis/flabk/pkg/asld"
|
||||
// "sectorinf.com/emilis/flabk/pkg/epk"
|
||||
// "github.com/stretchr/testify/require"
|
||||
// )
|
||||
"github.com/stretchr/testify/require"
|
||||
"sectorinf.com/emilis/flabk/pkg/epk"
|
||||
"sectorinf.com/emilis/flabk/pkg/ld"
|
||||
)
|
||||
|
||||
// type testObj struct {
|
||||
// String string `asld:"string" json:"string"`
|
||||
// NoTag string
|
||||
// Int int
|
||||
// Int8 int8
|
||||
// Int16 int16
|
||||
// Int32 int32
|
||||
// Int64 int64
|
||||
// Uint uint
|
||||
// Uint8 uint8
|
||||
// Uint16 uint16
|
||||
// Uint32 uint32
|
||||
// Uint64 uint64
|
||||
// Float32 float32
|
||||
// Float64 float64
|
||||
// // Complex64 complex64
|
||||
// // Complex128 complex128
|
||||
// IntPtr *int
|
||||
// Int8Ptr *int8
|
||||
// Int16Ptr *int16
|
||||
// Int32Ptr *int32
|
||||
// Int64Ptr *int64
|
||||
// UintPtr *uint
|
||||
// Uint8Ptr *uint8
|
||||
// Uint16Ptr *uint16
|
||||
// Uint32Ptr *uint32
|
||||
// Uint64Ptr *uint64
|
||||
// Float32Ptr *float32
|
||||
// Float64Ptr *float64
|
||||
// // Complex64Ptr *complex64
|
||||
// // Complex128Ptr *complex128
|
||||
// TestPtr *testObj
|
||||
// TestArray []testObj
|
||||
// }
|
||||
type testObj struct {
|
||||
String string `ld:"string" json:"string"`
|
||||
NoTag string
|
||||
Int int
|
||||
Int8 int8
|
||||
Int16 int16
|
||||
Int32 int32
|
||||
Int64 int64
|
||||
Uint uint
|
||||
Uint8 uint8
|
||||
Uint16 uint16
|
||||
Uint32 uint32
|
||||
Uint64 uint64
|
||||
Float32 float32
|
||||
Float64 float64
|
||||
// Complex64 complex64
|
||||
// Complex128 complex128
|
||||
IntPtr *int
|
||||
Int8Ptr *int8
|
||||
Int16Ptr *int16
|
||||
Int32Ptr *int32
|
||||
Int64Ptr *int64
|
||||
UintPtr *uint
|
||||
Uint8Ptr *uint8
|
||||
Uint16Ptr *uint16
|
||||
Uint32Ptr *uint32
|
||||
Uint64Ptr *uint64
|
||||
Float32Ptr *float32
|
||||
Float64Ptr *float64
|
||||
// Complex64Ptr *complex64
|
||||
// Complex128Ptr *complex128
|
||||
TestPtr *testObj
|
||||
TestArray []testObj
|
||||
}
|
||||
|
||||
// func TestUnmarshal(t *testing.T) {
|
||||
// tests := map[string]struct {
|
||||
// obj testObj
|
||||
// errCheck epk.ErrorTest
|
||||
// }{
|
||||
// "string children": {
|
||||
// obj: testObj{
|
||||
// String: "hello",
|
||||
// NoTag: "no_tag",
|
||||
// Int: math.MaxInt,
|
||||
// Int8: math.MaxInt8,
|
||||
// Int16: math.MaxInt16,
|
||||
// Int32: math.MaxInt32,
|
||||
// Int64: math.MaxInt64,
|
||||
// Uint: math.MaxUint,
|
||||
// Uint8: math.MaxUint8,
|
||||
// Uint16: math.MaxUint16,
|
||||
// Uint32: math.MaxUint32,
|
||||
// Uint64: math.MaxUint64,
|
||||
// Float32: math.MaxFloat32,
|
||||
// Float64: math.MaxFloat64,
|
||||
// TestPtr: &testObj{
|
||||
// String: "hello2",
|
||||
// },
|
||||
// TestArray: []testObj{
|
||||
// {
|
||||
// String: "hello3",
|
||||
// },
|
||||
// {
|
||||
// String: "hello4",
|
||||
// TestPtr: &testObj{
|
||||
// TestArray: []testObj{
|
||||
// {
|
||||
// String: "hello5",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// // Complex64: complex(math.MaxFloat32, math.MaxFloat32),
|
||||
// // Complex128: complex(math.MaxFloat64, math.MaxFloat64),
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
func TestUnmarshal(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
obj testObj
|
||||
errCheck epk.ErrorTest
|
||||
}{
|
||||
"string children": {
|
||||
obj: testObj{
|
||||
String: "hello",
|
||||
NoTag: "no_tag",
|
||||
Int: math.MaxInt,
|
||||
Int8: math.MaxInt8,
|
||||
Int16: math.MaxInt16,
|
||||
Int32: math.MaxInt32,
|
||||
Int64: math.MaxInt64,
|
||||
Uint: math.MaxUint,
|
||||
Uint8: math.MaxUint8,
|
||||
Uint16: math.MaxUint16,
|
||||
Uint32: math.MaxUint32,
|
||||
Uint64: math.MaxUint64,
|
||||
Float32: math.MaxFloat32,
|
||||
Float64: math.MaxFloat64,
|
||||
TestPtr: &testObj{
|
||||
String: "hello2",
|
||||
},
|
||||
TestArray: []testObj{
|
||||
{
|
||||
String: "hello3",
|
||||
},
|
||||
{
|
||||
String: "hello4",
|
||||
TestPtr: &testObj{
|
||||
TestArray: []testObj{
|
||||
{
|
||||
String: "hello5",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Complex64: complex(math.MaxFloat32, math.MaxFloat32),
|
||||
// Complex128: complex(math.MaxFloat64, math.MaxFloat64),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// for name, test := range tests {
|
||||
// test := test
|
||||
// t.Run(name, func(tt *testing.T) {
|
||||
// that := require.New(tt)
|
||||
// objJSON, err := json.MarshalIndent(test.obj, "", " ")
|
||||
// that.NoError(err)
|
||||
// result, err := asld.Unmarshal[testObj](objJSON)
|
||||
// that.NoError(err)
|
||||
// that.Equal(test.obj, result)
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
for name, test := range tests {
|
||||
test := test
|
||||
t.Run(name, func(tt *testing.T) {
|
||||
that := require.New(tt)
|
||||
objJSON, err := json.MarshalIndent(test.obj, "", " ")
|
||||
that.NoError(err)
|
||||
result, err := ld.Unmarshal[testObj](objJSON)
|
||||
that.NoError(err)
|
||||
that.Equal(test.obj, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// func TestBench(t *testing.T) {
|
||||
// obj := testObj{
|
||||
// String: "hello",
|
||||
// NoTag: "no_tag",
|
||||
// Int: math.MaxInt,
|
||||
// Int8: math.MaxInt8,
|
||||
// Int16: math.MaxInt16,
|
||||
// Int32: math.MaxInt32,
|
||||
// Int64: math.MaxInt64,
|
||||
// Uint: math.MaxUint,
|
||||
// Uint8: math.MaxUint8,
|
||||
// Uint16: math.MaxUint16,
|
||||
// Uint32: math.MaxUint32,
|
||||
// Uint64: math.MaxUint64,
|
||||
// Float32: math.MaxFloat32,
|
||||
// Float64: math.MaxFloat64,
|
||||
// // Complex64: complex(math.MaxFloat32, math.MaxFloat32),
|
||||
// // Complex128: complex(math.MaxFloat64, math.MaxFloat64),
|
||||
// }
|
||||
// that := require.New(t)
|
||||
// asldTotal := int64(0)
|
||||
// jsonTotal := int64(0)
|
||||
func TestBench(t *testing.T) {
|
||||
obj := testObj{
|
||||
String: "hello",
|
||||
NoTag: "no_tag",
|
||||
Int: math.MaxInt,
|
||||
Int8: math.MaxInt8,
|
||||
Int16: math.MaxInt16,
|
||||
Int32: math.MaxInt32,
|
||||
Int64: math.MaxInt64,
|
||||
Uint: math.MaxUint,
|
||||
Uint8: math.MaxUint8,
|
||||
Uint16: math.MaxUint16,
|
||||
Uint32: math.MaxUint32,
|
||||
Uint64: math.MaxUint64,
|
||||
Float32: math.MaxFloat32,
|
||||
Float64: math.MaxFloat64,
|
||||
// Complex64: complex(math.MaxFloat32, math.MaxFloat32),
|
||||
// Complex128: complex(math.MaxFloat64, math.MaxFloat64),
|
||||
}
|
||||
that := require.New(t)
|
||||
asldTotal := int64(0)
|
||||
jsonTotal := int64(0)
|
||||
|
||||
// jsonMax := int64(0)
|
||||
// jsonMin := math.MaxInt64
|
||||
jsonMax := int64(0)
|
||||
jsonMin := math.MaxInt64
|
||||
|
||||
// asldMax := int64(0)
|
||||
// asldMin := math.MaxInt64
|
||||
asldMax := int64(0)
|
||||
asldMin := math.MaxInt64
|
||||
|
||||
// count := int64(1 << 20)
|
||||
// for index := int64(0); index < count; index++ {
|
||||
// objJSON, err := json.Marshal(obj)
|
||||
// that.NoError(err)
|
||||
count := int64(1 << 20)
|
||||
for index := int64(0); index < count; index++ {
|
||||
objJSON, err := json.Marshal(obj)
|
||||
that.NoError(err)
|
||||
|
||||
// asldStart := time.Now()
|
||||
// _, err = asld.Unmarshal[testObj](objJSON)
|
||||
// asldDur := time.Since(asldStart)
|
||||
// asldTotal += int64(asldDur)
|
||||
// if asldDur < time.Duration(asldMin) {
|
||||
// asldMin = int(asldDur)
|
||||
// }
|
||||
// if asldDur > time.Duration(asldMax) {
|
||||
// asldMax = int64(asldDur)
|
||||
// }
|
||||
asldStart := time.Now()
|
||||
_, err = ld.Unmarshal[testObj](objJSON)
|
||||
asldDur := time.Since(asldStart)
|
||||
asldTotal += int64(asldDur)
|
||||
if asldDur < time.Duration(asldMin) {
|
||||
asldMin = int(asldDur)
|
||||
}
|
||||
if asldDur > time.Duration(asldMax) {
|
||||
asldMax = int64(asldDur)
|
||||
}
|
||||
|
||||
// that.NoError(err)
|
||||
// a := testObj{}
|
||||
that.NoError(err)
|
||||
a := testObj{}
|
||||
|
||||
// jsonStart := time.Now()
|
||||
// err = json.Unmarshal(objJSON, &a)
|
||||
// jsonDur := time.Since(jsonStart)
|
||||
// jsonTotal += int64(jsonDur)
|
||||
jsonStart := time.Now()
|
||||
err = json.Unmarshal(objJSON, &a)
|
||||
jsonDur := time.Since(jsonStart)
|
||||
jsonTotal += int64(jsonDur)
|
||||
|
||||
// if jsonDur < time.Duration(jsonMin) {
|
||||
// jsonMin = int(jsonDur)
|
||||
// }
|
||||
// if jsonDur > time.Duration(jsonMax) {
|
||||
// jsonMax = int64(jsonDur)
|
||||
// }
|
||||
if jsonDur < time.Duration(jsonMin) {
|
||||
jsonMin = int(jsonDur)
|
||||
}
|
||||
if jsonDur > time.Duration(jsonMax) {
|
||||
jsonMax = int64(jsonDur)
|
||||
}
|
||||
|
||||
// that.NoError(err)
|
||||
// }
|
||||
// fmt.Println(count, "runs")
|
||||
// fmt.Printf("json avg (%s), min (%s), max (%s)\n", time.Duration(jsonTotal/count), time.Duration(jsonMin), time.Duration(jsonMax))
|
||||
// fmt.Printf("asld avg (%s), min (%s), max (%s)\n", time.Duration(asldTotal/count), time.Duration(asldMin), time.Duration(asldMax))
|
||||
// }
|
||||
// func jsonElemList[T any](that *require.Assertions, a any) [][]byte {
|
||||
// that.NotNil(a)
|
||||
// v, ok := a.([]T)
|
||||
// that.True(ok)
|
||||
// fields := make([][]byte, len(v))
|
||||
// for index, field := range v {
|
||||
// jsonField, err := json.Marshal(field)
|
||||
// that.NoError(err)
|
||||
// fields[index] = jsonField
|
||||
// }
|
||||
// return fields
|
||||
// }
|
||||
that.NoError(err)
|
||||
}
|
||||
fmt.Println(count, "runs")
|
||||
fmt.Printf("json avg (%s), min (%s), max (%s)\n", time.Duration(jsonTotal/count), time.Duration(jsonMin), time.Duration(jsonMax))
|
||||
fmt.Printf("asld avg (%s), min (%s), max (%s)\n", time.Duration(asldTotal/count), time.Duration(asldMin), time.Duration(asldMax))
|
||||
}
|
||||
func jsonElemList[T any](that *require.Assertions, a any) [][]byte {
|
||||
that.NotNil(a)
|
||||
v, ok := a.([]T)
|
||||
that.True(ok)
|
||||
fields := make([][]byte, len(v))
|
||||
for index, field := range v {
|
||||
jsonField, err := json.Marshal(field)
|
||||
that.NoError(err)
|
||||
fields[index] = jsonField
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// type hello struct {
|
||||
// Hello string
|
||||
// }
|
||||
type hello struct {
|
||||
Hello string
|
||||
}
|
||||
|
||||
// func TestArrayMembers(t *testing.T) {
|
||||
// tests := map[string]struct {
|
||||
|
|
Loading…
Reference in New Issue