Feature complete. I may change the names, and error-handling needs to be more robust. I feel pretty good about resisting bad input, but not so good about saving to or writing from complex data structures, or maps with interface{} elements.
This commit is contained in:
parent
a19bd721cd
commit
10567f459b
50
ndb.go
50
ndb.go
@ -22,12 +22,12 @@
|
|||||||
package ndb
|
package ndb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"bytes"
|
|
||||||
"bufio"
|
"bufio"
|
||||||
"net/textproto"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/textproto"
|
||||||
|
"reflect"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -63,6 +63,7 @@ func (e *SyntaxError) Error() string {
|
|||||||
start := e.Offset
|
start := e.Offset
|
||||||
end := min(e.Offset+10, int64(len(e.Data)))
|
end := min(e.Offset+10, int64(len(e.Data)))
|
||||||
|
|
||||||
|
if e.Data != nil {
|
||||||
// Make sure we're on utf8 boundaries
|
// Make sure we're on utf8 boundaries
|
||||||
for !utf8.RuneStart(e.Data[start]) && start > 0 {
|
for !utf8.RuneStart(e.Data[start]) && start > 0 {
|
||||||
start--
|
start--
|
||||||
@ -70,15 +71,17 @@ func (e *SyntaxError) Error() string {
|
|||||||
for !utf8.Valid(e.Data[start:end]) && end < int64(len(e.Data)) {
|
for !utf8.Valid(e.Data[start:end]) && end < int64(len(e.Data)) {
|
||||||
end++
|
end++
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s\n\tat `%s'", e.Message, e.Data[start:end])
|
return fmt.Sprintf("%s\n\tat `%s'", e.Message, e.Data[start:end])
|
||||||
}
|
}
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
// An Encoder wraps an io.Writer and serializes Go values
|
// An Encoder wraps an io.Writer and serializes Go values
|
||||||
// into ndb strings. Successive calls to the Encode() method
|
// into ndb strings. Successive calls to the Encode() method
|
||||||
// append lines to the io.Writer.
|
// append lines to the io.Writer.
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
out bufio.Writer
|
start bool
|
||||||
|
out io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// A decoder wraps an io.Reader and decodes successive ndb strings
|
// A decoder wraps an io.Reader and decodes successive ndb strings
|
||||||
@ -174,7 +177,12 @@ func (d *Decoder) Decode(v interface{}) error {
|
|||||||
// valid ndb strings, an error is returned. No guarantee is made about
|
// valid ndb strings, an error is returned. No guarantee is made about
|
||||||
// the order of the tuples.
|
// the order of the tuples.
|
||||||
func Emit(v interface{}) ([]byte, error) {
|
func Emit(v interface{}) ([]byte, error) {
|
||||||
return nil,nil
|
var buf bytes.Buffer
|
||||||
|
e := NewEncoder(&buf)
|
||||||
|
if err := e.Encode(v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Encode method will write the ndb encoding of the Go value v
|
// The Encode method will write the ndb encoding of the Go value v
|
||||||
@ -183,16 +191,32 @@ func Emit(v interface{}) ([]byte, error) {
|
|||||||
// If the value cannot be fully encoded, an error is returned and
|
// If the value cannot be fully encoded, an error is returned and
|
||||||
// no data will be written to the io.Writer.
|
// no data will be written to the io.Writer.
|
||||||
func (e *Encoder) Encode(v interface{}) error {
|
func (e *Encoder) Encode(v interface{}) error {
|
||||||
return nil
|
val := reflect.ValueOf(v)
|
||||||
|
// Drill down to the concrete value
|
||||||
|
for val.Kind() == reflect.Ptr {
|
||||||
|
if val.IsNil() {
|
||||||
|
return &TypeError{nil}
|
||||||
|
} else {
|
||||||
|
val = val.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
e.start = false
|
||||||
|
}()
|
||||||
|
switch val.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
return e.encodeSlice(val)
|
||||||
|
case reflect.Struct:
|
||||||
|
return e.encodeStruct(val)
|
||||||
|
case reflect.Map:
|
||||||
|
return e.encodeMap(val)
|
||||||
|
default:
|
||||||
|
return &TypeError{val.Type()}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEncoder returns an Encoder that writes ndb output to an
|
// NewEncoder returns an Encoder that writes ndb output to an
|
||||||
// io.Writer
|
// io.Writer
|
||||||
func NewEncoder(w io.Writer) *Encoder {
|
func NewEncoder(w io.Writer) *Encoder {
|
||||||
return nil
|
return &Encoder{out: w}
|
||||||
}
|
|
||||||
|
|
||||||
// Flush forces all outstanding data in an Encoder to be written to
|
|
||||||
// its backend io.Writer.
|
|
||||||
func (e *Encoder) Flush() {
|
|
||||||
}
|
}
|
||||||
|
15
read.go
15
read.go
@ -1,14 +1,14 @@
|
|||||||
package ndb
|
package ndb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"reflect"
|
|
||||||
"net/textproto"
|
|
||||||
"unicode"
|
|
||||||
"strconv"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"strings"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/textproto"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
type scanner struct {
|
type scanner struct {
|
||||||
@ -203,6 +203,7 @@ func storeVal(dst reflect.Value, src []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type scanState []int
|
type scanState []int
|
||||||
|
|
||||||
func (s *scanState) push(n int) {
|
func (s *scanState) push(n int) {
|
||||||
*s = append(*s, n)
|
*s = append(*s, n)
|
||||||
}
|
}
|
||||||
@ -277,7 +278,7 @@ func (d *Decoder) parseLine(line []byte) ([]pair, error) {
|
|||||||
}
|
}
|
||||||
state.pop()
|
state.pop()
|
||||||
state.push(scanValueStart)
|
state.push(scanValueStart)
|
||||||
} else if !(unicode.IsLetter(r) || unicode.IsNumber(r)) {
|
} else if !(r == '-' || unicode.IsLetter(r) || unicode.IsNumber(r)) {
|
||||||
return nil, errBadAttr(line, offset)
|
return nil, errBadAttr(line, offset)
|
||||||
}
|
}
|
||||||
case scanValueStart:
|
case scanValueStart:
|
||||||
|
58
read_test.go
58
read_test.go
@ -1,8 +1,9 @@
|
|||||||
package ndb
|
package ndb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type screenCfg struct {
|
type screenCfg struct {
|
||||||
@ -12,9 +13,9 @@ type screenCfg struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type netCfg struct {
|
type netCfg struct {
|
||||||
Host string `ndb:"hostname"`
|
Host string `ndb:"host-name"`
|
||||||
Vlan []int `ndb:"vlan"`
|
Vlan []int `ndb:"vlan"`
|
||||||
Native int `ndb:"nativevlan"`
|
Native int `ndb:"native-vlan"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var multiMap = []struct {
|
var multiMap = []struct {
|
||||||
@ -34,7 +35,7 @@ var advancedTests = []struct {
|
|||||||
in string
|
in string
|
||||||
out netCfg
|
out netCfg
|
||||||
}{
|
}{
|
||||||
{ in: "hostname=p2-jbs537 vlan=66 vlan=35 nativevlan=218",
|
{in: "host-name=p2-jbs537 vlan=66 vlan=35 native-vlan=218",
|
||||||
out: netCfg{
|
out: netCfg{
|
||||||
Host: "p2-jbs537",
|
Host: "p2-jbs537",
|
||||||
Vlan: []int{66, 35},
|
Vlan: []int{66, 35},
|
||||||
@ -63,7 +64,7 @@ var mapTests = []struct {
|
|||||||
out map[string]string
|
out map[string]string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
in: "ipnet=murray–hill ip=135.104.0.0 ipmask=255.255.0.0",
|
in: "ipnet=murray-hill ip=135.104.0.0 ipmask=255.255.0.0",
|
||||||
out: map[string]string{
|
out: map[string]string{
|
||||||
"ipnet": "murray-hill",
|
"ipnet": "murray-hill",
|
||||||
"ip": "135.104.0.0",
|
"ip": "135.104.0.0",
|
||||||
@ -89,13 +90,27 @@ func TestMap(t *testing.T) {
|
|||||||
for _, tt := range mapTests {
|
for _, tt := range mapTests {
|
||||||
if err := Parse([]byte(tt.in), &net); err != nil {
|
if err := Parse([]byte(tt.in), &net); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
} else if fmt.Sprint(net) != fmt.Sprint(tt.out) {
|
} else if !mapEquals(tt.out, net) {
|
||||||
t.Errorf("Got %v, wanted %v", net, tt.out)
|
t.Errorf("Got `%v`, wanted `%v`", net, tt.out)
|
||||||
}
|
}
|
||||||
t.Logf("%s == %v", tt.in, net)
|
t.Logf("%s == %v", tt.in, net)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mapEquals(m1, m2 map[string] string) bool {
|
||||||
|
for k := range m1 {
|
||||||
|
if m1[k] != m2[k] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k := range m2 {
|
||||||
|
if m1[k] != m2[k] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func TestAdvanced(t *testing.T) {
|
func TestAdvanced(t *testing.T) {
|
||||||
var net netCfg
|
var net netCfg
|
||||||
for _, tt := range advancedTests {
|
for _, tt := range advancedTests {
|
||||||
@ -120,34 +135,6 @@ func TestMultiMap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func netEqual(t *testing.T, n1, n2 netCfg) bool {
|
|
||||||
if len(n1.Vlan) != len(n2.Vlan) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := range n1.Vlan {
|
|
||||||
if n1.Vlan[i] != n2.Vlan[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n1.Host == n2.Host && n1.Native == n2.Native
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapEqual(t *testing.T, m1, m2 map[string] string) bool {
|
|
||||||
for k := range m1 {
|
|
||||||
if m1[k] != m2[k] {
|
|
||||||
t.Logf("%v != %v", m1[k], m2[k])
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
package ndb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"bytes"
|
|
||||||
)
|
|
||||||
|
|
||||||
var parseTests = []struct {
|
var parseTests = []struct {
|
||||||
in []byte
|
in []byte
|
||||||
out []pair
|
out []pair
|
||||||
@ -197,7 +184,6 @@ var parseTests = []struct {
|
|||||||
out: []pair{
|
out: []pair{
|
||||||
{[]byte("esc"), []byte("Use '' to escape a '")}},
|
{[]byte("esc"), []byte("Use '' to escape a '")}},
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_parsing(t *testing.T) {
|
func Test_parsing(t *testing.T) {
|
||||||
|
131
write.go
131
write.go
@ -1 +1,132 @@
|
|||||||
package ndb
|
package ndb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *Encoder) encodeSlice(val reflect.Value) error {
|
||||||
|
for i := 0; i < val.Len(); i++ {
|
||||||
|
e.Encode(val.Index(i).Interface())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) encodeStruct(val reflect.Value) error {
|
||||||
|
typ := val.Type()
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
ft := typ.Field(i)
|
||||||
|
attr := ft.Name
|
||||||
|
if tag := ft.Tag.Get("ndb"); tag != "" {
|
||||||
|
attr = tag
|
||||||
|
}
|
||||||
|
err := e.writeTuple(attr, val.Field(i))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) encodeMap(val reflect.Value) error {
|
||||||
|
for _, k := range val.MapKeys() {
|
||||||
|
v := val.MapIndex(k)
|
||||||
|
|
||||||
|
if err := e.writeTuple(k.Interface(), v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) writeTuple(k interface{}, v reflect.Value) error {
|
||||||
|
var values reflect.Value
|
||||||
|
var attrBuf, valBuf bytes.Buffer
|
||||||
|
fmt.Fprint(&attrBuf, k)
|
||||||
|
|
||||||
|
attr := attrBuf.Bytes()
|
||||||
|
|
||||||
|
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||||
|
sliceType := reflect.SliceOf(v.Type())
|
||||||
|
pv := reflect.New(sliceType)
|
||||||
|
pv.Elem().Set(reflect.MakeSlice(sliceType, 0, 1))
|
||||||
|
pv.Elem().Set(reflect.Append(pv.Elem(), v))
|
||||||
|
values = pv.Elem()
|
||||||
|
} else {
|
||||||
|
values = v
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < values.Len(); i++ {
|
||||||
|
fmt.Fprint(&valBuf, values.Index(i).Interface())
|
||||||
|
val := valBuf.Bytes()
|
||||||
|
if e.start {
|
||||||
|
if _, err := e.out.Write([]byte{' '}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
e.start = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validAttr(attr) {
|
||||||
|
return &SyntaxError{nil, 0, fmt.Sprintf("Invalid attribute %s", attr)}
|
||||||
|
}
|
||||||
|
if !validVal(val) {
|
||||||
|
return &SyntaxError{nil, 0, fmt.Sprintf("Invalid value %s", val)}
|
||||||
|
}
|
||||||
|
if bytes.IndexByte(val, '\'') != -1 {
|
||||||
|
val = bytes.Replace(val, []byte{'\''}, []byte{'\'', '\''}, -1)
|
||||||
|
}
|
||||||
|
if _, err := e.out.Write(attr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := e.out.Write([]byte{'='}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
x := bytes.IndexFunc(val, func(r rune) bool {
|
||||||
|
return unicode.IsSpace(r)
|
||||||
|
})
|
||||||
|
if x != -1 {
|
||||||
|
if _, err := e.out.Write([]byte{'\''}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := e.out.Write(val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if x != -1 {
|
||||||
|
if _, err := e.out.Write([]byte{'\''}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
valBuf.Reset()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validAttr(attr []byte) bool {
|
||||||
|
if !utf8.Valid(attr) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
x := bytes.IndexFunc(attr, func(r rune) bool {
|
||||||
|
switch {
|
||||||
|
case r == '\'':
|
||||||
|
return true
|
||||||
|
case unicode.IsSpace(r):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !unicode.IsLetter(r) &&
|
||||||
|
!unicode.IsNumber(r) &&
|
||||||
|
r != '-'
|
||||||
|
})
|
||||||
|
return x == -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func validVal(val []byte) bool {
|
||||||
|
if !utf8.Valid(val) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return bytes.IndexByte(val, '\n') == -1
|
||||||
|
}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
package ndb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var structWriteTests = []struct {
|
||||||
|
in netCfg
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
netCfg{"p2-jbs239", []int{64, 52, 100}, 666},
|
||||||
|
"host-name=p2-jbs239 vlan=64 vlan=52 vlan=100 native-vlan=666",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
netCfg{"p2-cass304", []int{55, 10}, 1},
|
||||||
|
"host-name=p2-cass304 vlan=55 vlan=10 native-vlan=1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapWriteTests = []struct {
|
||||||
|
in map[string] string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
map[string] string {"user": "jenkins", "group": "jenkins"},
|
||||||
|
"user=jenkins group=jenkins",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructWrite(t *testing.T) {
|
||||||
|
for _, tt := range structWriteTests {
|
||||||
|
if b, err := Emit(tt.in); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if string(b) != tt.out {
|
||||||
|
t.Errorf("Wanted %s, got %s", tt.out, string(b))
|
||||||
|
} else {
|
||||||
|
t.Logf("%v => %s", tt.in, string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapWrite(t *testing.T) {
|
||||||
|
for _, tt := range mapWriteTests {
|
||||||
|
if b, err := Emit(tt.in); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if string(b) != tt.out {
|
||||||
|
t.Errorf("Wanted %s, got %s", tt.out, string(b))
|
||||||
|
} else {
|
||||||
|
t.Logf("%v => %s", tt.in, string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user