diff --git a/client/go/internal/vespa/slime/error.go b/client/go/internal/vespa/slime/error.go new file mode 100644 index 000000000000..177b6c665aa3 --- /dev/null +++ b/client/go/internal/vespa/slime/error.go @@ -0,0 +1,32 @@ +// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package slime + +import "errors" + +var ( + Invalid Value = ErrorMsg("invalid value") +) + +type errorValue struct { + emptyValue + err error +} + +func ErrorMsg(msg string) Value { + return &errorValue{err: errors.New(msg)} +} + +func Error(err error) Value { + return &errorValue{err: err} +} + +func AsError(value Value) error { + ev, ok := value.(*errorValue) + if ok { + return ev.err + } + return nil +} + +func (*errorValue) Valid() bool { return false } diff --git a/client/go/internal/vespa/slime/inserter.go b/client/go/internal/vespa/slime/inserter.go new file mode 100644 index 000000000000..5bc91790fd2f --- /dev/null +++ b/client/go/internal/vespa/slime/inserter.go @@ -0,0 +1,32 @@ +// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package slime + +type Inserter interface { + Insert(value Value) Value +} + +type myInserter func(value Value) Value + +func (f myInserter) Insert(value Value) Value { + return f(value) +} + +func InsertRoot(root *Value) Inserter { + return myInserter(func(value Value) Value { + *root = value + return value + }) +} + +func InsertEntry(arr Value) Inserter { + return myInserter(func(value Value) Value { + return arr.Add(value) + }) +} + +func InsertField(obj Value, name string) Inserter { + return myInserter(func(value Value) Value { + return obj.Set(name, value) + }) +} diff --git a/client/go/internal/vespa/slime/inserter_test.go b/client/go/internal/vespa/slime/inserter_test.go new file mode 100644 index 000000000000..4ae14c122d1b --- /dev/null +++ b/client/go/internal/vespa/slime/inserter_test.go @@ -0,0 +1,32 @@ +// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package slime + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestInsertRoot(t *testing.T) { + var root Value + in := InsertRoot(&root) + res := in.Insert(Long(5)) + assert.Equal(t, int64(5), root.AsLong()) + assert.Equal(t, int64(5), res.AsLong()) +} + +func TestInsertEntry(t *testing.T) { + arr := Array() + in := InsertEntry(arr) + res := in.Insert(Long(5)) + assert.Equal(t, int64(5), arr.Entry(0).AsLong()) + assert.Equal(t, int64(5), res.AsLong()) +} + +func TestInsertField(t *testing.T) { + obj := Object() + in := InsertField(obj, "foo") + res := in.Insert(Long(5)) + assert.Equal(t, int64(5), obj.Field("foo").AsLong()) + assert.Equal(t, int64(5), res.AsLong()) +} diff --git a/client/go/internal/vespa/slime/json.go b/client/go/internal/vespa/slime/json.go new file mode 100644 index 000000000000..fa8502d14217 --- /dev/null +++ b/client/go/internal/vespa/slime/json.go @@ -0,0 +1,543 @@ +// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package slime + +import ( + "bytes" + "errors" + "fmt" + "io" + "math" + "strconv" + "strings" +) + +var ( + hex []byte = []byte("0123456789ABCDEF") +) + +func fromHexDigit(digit byte) int { + switch digit { + case byte('0'): + return 0 + case byte('1'): + return 1 + case byte('2'): + return 2 + case byte('3'): + return 3 + case byte('4'): + return 4 + case byte('5'): + return 5 + case byte('6'): + return 6 + case byte('7'): + return 7 + case byte('8'): + return 8 + case byte('9'): + return 9 + case byte('a'), byte('A'): + return 0xA + case byte('b'), byte('B'): + return 0xB + case byte('c'), byte('C'): + return 0xC + case byte('d'), byte('D'): + return 0xD + case byte('e'), byte('E'): + return 0xE + case byte('f'), byte('F'): + return 0xF + default: + return -1 + } +} + +func ToJson(value Value, compact bool) string { + var out strings.Builder + err := EncodeJson(value, compact, &out) + if err != nil { + panic("error writing to string builder") + } + return out.String() +} + +func EncodeJson(value Value, compact bool, output io.Writer) error { + encoder := &jsonEncoder{output: output, compact: compact, first: true} + encoder.encode(value) + return encoder.err +} + +type jsonEncoder struct { + output io.Writer + compact bool + level int + first bool + err error +} + +func (e *jsonEncoder) fmt(format string, stuff ...any) { + if e.err == nil { + _, e.err = fmt.Fprintf(e.output, format, stuff...) + } +} + +func (e *jsonEncoder) put(value byte) { + if e.err == nil { + _, e.err = e.output.Write([]byte{value}) + } +} + +func (e *jsonEncoder) openScope(tag byte) { + e.put(tag) + e.level++ + e.first = true +} + +func (e *jsonEncoder) separate(useComma bool) { + if !e.first && useComma { + e.put(byte(',')) + } else { + e.first = false + } + if !e.compact { + e.fmt("\n%*s", e.level*4, "") + } +} + +func (e *jsonEncoder) closeScope(tag byte) { + e.level-- + e.separate(false) + e.put(tag) +} + +func (e *jsonEncoder) encode(value Value) { + e.encodeValue(value) + if !e.compact { + e.put(byte('\n')) + } +} + +func (e *jsonEncoder) encodeValue(value Value) { + switch value.Type() { + case EMPTY: + e.encodeEMPTY() + case BOOL: + e.encodeBOOL(value.AsBool()) + case LONG: + e.encodeLONG(value.AsLong()) + case DOUBLE: + e.encodeDOUBLE(value.AsDouble()) + case STRING: + e.encodeSTRING(value.AsString()) + case DATA: + e.encodeDATA(value.AsData()) + case ARRAY: + e.encodeARRAY(value) + case OBJECT: + e.encodeOBJECT(value) + } +} + +func (e *jsonEncoder) encodeEMPTY() { + e.fmt("null") +} + +func (e *jsonEncoder) encodeBOOL(value bool) { + if value { + e.fmt("true") + } else { + e.fmt("false") + } +} + +func (e *jsonEncoder) encodeLONG(value int64) { + e.fmt("%v", value) +} + +func (e *jsonEncoder) encodeDOUBLE(value float64) { + if math.IsNaN(value) || math.IsInf(value, 0) { + e.fmt("null") + } else { + e.fmt("%v", value) + } +} + +func (e *jsonEncoder) encodeSTRING(value string) { + e.put(byte('"')) + for i := 0; i < len(value); i++ { + switch c := value[i]; c { + case '"': + e.fmt("\\\"") + case '\\': + e.fmt("\\\\") + case '\b': + e.fmt("\\b") + case '\f': + e.fmt("\\f") + case '\n': + e.fmt("\\n") + case '\r': + e.fmt("\\r") + case '\t': + e.fmt("\\t") + default: + if c > 0x1f { + e.put(c) + } else { + e.fmt("\\u00") + e.put(hex[(c>>4)&0xf]) + e.put(hex[c&0xf]) + } + } + } + e.put(byte('"')) +} + +func (e *jsonEncoder) encodeDATA(value []byte) { + e.fmt("\"0x") + for i := 0; i < len(value); i++ { + c := value[i] + e.put(hex[(c>>4)&0xf]) + e.put(hex[c&0xf]) + } + e.put(byte('"')) +} + +func (e *jsonEncoder) encodeARRAY(value Value) { + e.openScope(byte('[')) + value.EachEntry(func(idx int, val Value) { + e.separate(true) + e.encodeValue(val) + }) + e.closeScope(byte(']')) +} + +func (e *jsonEncoder) encodeOBJECT(value Value) { + e.openScope(byte('{')) + value.EachField(func(name string, val Value) { + e.separate(true) + e.encodeSTRING(name) + if e.compact { + e.put(byte(':')) + } else { + e.fmt(": ") + } + e.encodeValue(val) + }) + e.closeScope(byte('}')) +} + +func FromJson(input string) Value { + return DecodeJson(strings.NewReader(input)) +} + +func DecodeJson(input io.Reader) Value { + decoder := &jsonDecoder{input: input, buf: make([]byte, 1)} + return decoder.decode() +} + +type jsonDecoder struct { + input io.Reader + buf []byte + c byte + key bytes.Buffer + val bytes.Buffer + err error +} + +func (d *jsonDecoder) next() { + if d.err == nil { + len, e := d.input.Read(d.buf) + if len == 1 { + d.c = d.buf[0] + } else { + if e != nil { + d.err = e + } else { + d.err = errors.New("read failed without error") + } + d.c = 0 + } + } else if d.err == io.EOF { + d.err = errors.New("input underflow") + } +} + +func (d *jsonDecoder) fail(msg string) { + if d.err == nil || d.err == io.EOF { + d.err = errors.New(msg) + d.c = 0 + } +} + +func (d *jsonDecoder) failWithError(err error) { + if d.err == nil { + d.err = err + d.c = 0 + } +} + +func (d *jsonDecoder) skip(x byte) bool { + if d.c != x { + return false + } + d.next() + return true +} + +func (d *jsonDecoder) expect(str string) { + for i := 0; i < len(str); i++ { + if !d.skip(str[i]) { + d.fail("unexpected character") + return + } + } +} + +func (d *jsonDecoder) skipWhiteSpace() { + for { + switch d.c { + case byte(' '), byte('\t'), byte('\n'), byte('\r'): + d.next() + default: + return + } + } +} + +func (d *jsonDecoder) decode() Value { + var result Value + d.next() + d.decodeValue(InsertRoot(&result)) + if result != nil && errors.Is(d.err, io.EOF) { + d.err = nil + } + if d.err != nil { + return Error(d.err) + } + if result == nil { + return ErrorMsg("missing value") + } + return result +} + +func (d *jsonDecoder) decodeValue(inserter Inserter) { + d.skipWhiteSpace() + switch d.c { + case byte('"'), byte('\''): + d.decodeString(inserter) + case byte('{'): + d.decodeObject(inserter) + case byte('['): + d.decodeArray(inserter) + case byte('t'): + d.expect("true") + inserter.Insert(Bool(true)) + case byte('f'): + d.expect("false") + inserter.Insert(Bool(false)) + case byte('n'): + d.expect("null") + inserter.Insert(Empty) + case byte('x'): + d.decodeData(inserter) + case byte('-'), byte('0'), byte('1'), byte('2'), byte('3'), byte('4'), byte('5'), byte('6'), byte('7'), byte('8'), byte('9'): + d.decodeNumber(inserter) + default: + d.fail("invalid initial character for value") + } +} + +func (d *jsonDecoder) readHexRune() rune { + var value rune + for i := 0; i < 4; i++ { + x := fromHexDigit(d.c) + if x < 0 { + d.fail("invalid hex character") + return 0 + } + value = (value << 4) | rune(x) + d.next() + } + return value +} + +func (d *jsonDecoder) unescapeUtf16() rune { + d.expect("u") + codepoint := d.readHexRune() + if codepoint >= 0xd800 { + if codepoint < 0xdc00 { // high + d.expect("\\u") + low := d.readHexRune() + if low >= 0xdc00 && low < 0xe000 { + codepoint = 0x10000 + ((codepoint - 0xd800) << 10) + (low - 0xdc00) + } else { + d.fail("missing low surrogate") + } + } else if codepoint < 0xe000 { // low + d.fail("unexpected low surrogate") + } + } + return codepoint +} + +func (d *jsonDecoder) readString(str *bytes.Buffer) { + str.Reset() + quote := d.c + d.next() + for { + switch d.c { + case byte('\\'): + d.next() + switch d.c { + case byte('"'), byte('\\'), byte('/'), byte('\''): + str.WriteByte(d.c) + case byte('b'): + str.WriteByte(byte('\b')) + case byte('f'): + str.WriteByte(byte('\f')) + case byte('n'): + str.WriteByte(byte('\n')) + case byte('r'): + str.WriteByte(byte('\r')) + case byte('t'): + str.WriteByte(byte('\t')) + case byte('u'): + str.WriteRune(d.unescapeUtf16()) + continue + default: + d.fail("invalid quoted char") + } + d.next() + case byte('"'), byte('\''): + if d.c == quote { + d.next() + return + } else { + str.WriteByte(d.c) + d.next() + } + case 0: + d.fail("unterminated string") + return + default: + str.WriteByte(d.c) + d.next() + } + } +} + +func (d *jsonDecoder) readKey() { + switch d.c { + case byte('"'), byte('\''): + d.readString(&d.key) + return + default: + d.key.Reset() + for { + switch d.c { + case byte(':'), byte(' '), byte('\t'), byte('\n'), byte('\r'), 0: + return + default: + d.key.WriteByte(d.c) + d.next() + } + } + } +} + +func (d *jsonDecoder) decodeString(inserter Inserter) { + d.readString(&d.val) + inserter.Insert(String(d.val.String())) +} + +func (d *jsonDecoder) decodeObject(inserter Inserter) { + obj := inserter.Insert(Object()) + d.expect("{") + d.skipWhiteSpace() + if d.c != byte('}') { + for again := true; again; again = d.skip(byte(',')) { + d.skipWhiteSpace() + d.readKey() + d.skipWhiteSpace() + d.expect(":") + d.decodeValue(InsertField(obj, d.key.String())) + d.skipWhiteSpace() + } + } + d.expect("}") +} + +func (d *jsonDecoder) decodeArray(inserter Inserter) { + arr := inserter.Insert(Array()) + arrInserter := InsertEntry(arr) + d.expect("[") + d.skipWhiteSpace() + if d.c != byte(']') { + for again := true; again; again = d.skip(byte(',')) { + d.decodeValue(arrInserter) + d.skipWhiteSpace() + } + } + d.expect("]") +} + +func (d *jsonDecoder) decodeData(inserter Inserter) { + d.val.Reset() + for { + d.next() + hi := fromHexDigit(d.c) + if hi < 0 { + tmp := make([]byte, d.val.Len()) + copy(tmp, d.val.Bytes()) + inserter.Insert(Data(tmp)) + return + } + d.next() + lo := fromHexDigit(d.c) + if lo < 0 { + d.fail("invalid hex dump") + return + } + d.val.WriteByte(byte((hi << 4) | lo)) + } +} + +func (d *jsonDecoder) decodeNumber(inserter Inserter) { + isLong := true + d.val.Reset() + d.val.WriteByte(d.c) + d.next() + for { + switch d.c { + case byte('+'), byte('-'), byte('.'), byte('e'), byte('E'): + isLong = false + d.val.WriteByte(d.c) + d.next() + case byte('0'), byte('1'), byte('2'), byte('3'), byte('4'), + byte('5'), byte('6'), byte('7'), byte('8'), byte('9'): + d.val.WriteByte(d.c) + d.next() + default: + if isLong { + x, err := strconv.ParseInt(d.val.String(), 10, 64) + if err != nil { + d.failWithError(err) + } else { + inserter.Insert(Long(x)) + } + } else { + x, err := strconv.ParseFloat(d.val.String(), 64) + if err != nil { + d.failWithError(err) + } else { + inserter.Insert(Double(x)) + } + } + return + } + } +} diff --git a/client/go/internal/vespa/slime/json_test.go b/client/go/internal/vespa/slime/json_test.go new file mode 100644 index 000000000000..7336ed10454f --- /dev/null +++ b/client/go/internal/vespa/slime/json_test.go @@ -0,0 +1,427 @@ +// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package slime + +import ( + "errors" + "github.com/stretchr/testify/assert" + "math" + "strings" + "testing" +) + +func checkJson(t *testing.T, value Value, compact bool, expect ...string) { + actual := ToJson(value, compact) + if len(expect) == 1 { + assert.Equal(t, expect[0], actual) + } else { + for _, e := range expect { + if actual == e { + return + } + } + t.Errorf("result '%s' did not match expectations: \n%s\n", + actual, strings.Join(expect, "\n OR \n")) + } +} + +type verifyValue func(t *testing.T, actual Value) + +func verifyInvalid() verifyValue { + return func(t *testing.T, actual Value) { + assert.False(t, actual.Valid()) + assert.Equal(t, EMPTY, actual.Type()) + } +} + +func verifyEmpty() verifyValue { + return func(t *testing.T, actual Value) { + assert.True(t, actual.Valid()) + assert.Equal(t, EMPTY, actual.Type()) + } +} + +func verifyBool(expect bool) verifyValue { + return func(t *testing.T, actual Value) { + assert.True(t, actual.Valid()) + assert.Equal(t, BOOL, actual.Type()) + assert.Equal(t, expect, actual.AsBool()) + } +} + +func verifyLong(expect int64) verifyValue { + return func(t *testing.T, actual Value) { + assert.True(t, actual.Valid()) + assert.Equal(t, LONG, actual.Type()) + assert.Equal(t, expect, actual.AsLong()) + assert.Equal(t, float64(expect), actual.AsDouble()) + } +} + +func verifyDouble(expect float64) verifyValue { + return func(t *testing.T, actual Value) { + assert.True(t, actual.Valid()) + assert.Equal(t, DOUBLE, actual.Type()) + assert.Equal(t, expect, actual.AsDouble()) + assert.Equal(t, int64(expect), actual.AsLong()) + } +} + +func verifyString(expect string) verifyValue { + return func(t *testing.T, actual Value) { + assert.True(t, actual.Valid()) + assert.Equal(t, STRING, actual.Type()) + assert.Equal(t, expect, actual.AsString()) + } +} + +func verifyData(expect []byte) verifyValue { + return func(t *testing.T, actual Value) { + assert.True(t, actual.Valid()) + assert.Equal(t, DATA, actual.Type()) + assert.Equal(t, expect, actual.AsData()) + } +} + +func verifyArray(expect []verifyValue) verifyValue { + return func(t *testing.T, actual Value) { + assert.True(t, actual.Valid()) + assert.Equal(t, ARRAY, actual.Type()) + assert.Equal(t, len(expect), actual.NumEntries()) + for i, e := range expect { + e(t, actual.Entry(i)) + } + } +} + +func verifyObject(expect map[string]verifyValue) verifyValue { + return func(t *testing.T, actual Value) { + assert.True(t, actual.Valid()) + assert.Equal(t, OBJECT, actual.Type()) + assert.Equal(t, len(expect), actual.NumFields()) + for n, e := range expect { + e(t, actual.Field(n)) + } + } +} + +func verifyFromValue(value Value) verifyValue { + switch value.Type() { + case EMPTY: + return verifyEmpty() + case BOOL: + return verifyBool(value.AsBool()) + case LONG: + return verifyLong(value.AsLong()) + case DOUBLE: + return verifyDouble(value.AsDouble()) + case STRING: + return verifyString(value.AsString()) + case DATA: + return verifyData(value.AsData()) + case ARRAY: + var entries []verifyValue + value.EachEntry(func(idx int, val Value) { + entries = append(entries, verifyFromValue(val)) + }) + return verifyArray(entries) + case OBJECT: + fields := make(map[string]verifyValue) + value.EachField(func(name string, val Value) { + fields[name] = verifyFromValue(val) + }) + return verifyObject(fields) + } + return func(t *testing.T, actual Value) { + assert.Fail(t, "verifyValue using non-existing type") + } +} + +func verifyFromJson(json string) verifyValue { + expect := FromJson(json) + if !expect.Valid() { + return func(t *testing.T, actual Value) { + assert.Fail(t, "verifyValue created from invalid json") + } + } + return verifyFromValue(expect) +} + +func TestJsonEncodeEmpty(t *testing.T) { + checkJson(t, Empty, true, "null") + checkJson(t, Invalid, true, "null") + checkJson(t, Empty, false, "null\n") +} + +func TestJsonEncodeBool(t *testing.T) { + checkJson(t, Bool(false), true, "false") + checkJson(t, Bool(true), true, "true") + checkJson(t, Bool(false), false, "false\n") +} + +func TestJsonEncodeLong(t *testing.T) { + checkJson(t, Long(0), true, "0") + checkJson(t, Long(5), true, "5") + checkJson(t, Long(7), true, "7") + checkJson(t, Long(7), false, "7\n") +} + +func TestJsonEncodeDouble(t *testing.T) { + checkJson(t, Double(0.0), true, "0") + checkJson(t, Double(5.0), true, "5") + checkJson(t, Double(7.5), true, "7.5") + checkJson(t, Double(math.NaN()), true, "null") + checkJson(t, Double(math.Inf(1)), true, "null") + checkJson(t, Double(math.Inf(-1)), true, "null") + checkJson(t, Double(7.5), false, "7.5\n") +} + +func TestJsonEncodeString(t *testing.T) { + checkJson(t, String(""), true, "\"\"") + checkJson(t, String("foo"), true, "\"foo\"") + checkJson(t, String("bar"), true, "\"bar\"") + checkJson(t, String("\""), true, "\"\\\"\"") + checkJson(t, String("\\"), true, "\"\\\\\"") + checkJson(t, String("\b"), true, "\"\\b\"") + checkJson(t, String("\f"), true, "\"\\f\"") + checkJson(t, String("\n"), true, "\"\\n\"") + checkJson(t, String("\r"), true, "\"\\r\"") + checkJson(t, String("\t"), true, "\"\\t\"") + checkJson(t, String("\x1f"), true, "\"\\u001F\"") + checkJson(t, String("\x20"), true, "\" \"") + checkJson(t, String("%"), true, "\"%\"") + checkJson(t, String("bar"), false, "\"bar\"\n") +} + +func TestJsonEncodeData(t *testing.T) { + checkJson(t, Data(emptyBytes), true, "\"0x\"") + buf := make([]byte, 8) + for i := 0; i < 8; i++ { + buf[i] = byte(((i * 2) << 4) | (i*2 + 1)) + } + checkJson(t, Data(buf), true, "\"0x0123456789ABCDEF\"") + checkJson(t, Data(buf), false, "\"0x0123456789ABCDEF\"\n") +} + +func TestJsonEncodeArray(t *testing.T) { + arr := Array() + arr.Add(Bool(true)) + arr.Add(String("foo")) + checkJson(t, arr, true, "[true,\"foo\"]") + checkJson(t, arr, false, "[\n true,\n \"foo\"\n]\n") +} + +func TestJsonEncodeObject(t *testing.T) { + obj := Object() + obj.Set("foo", Bool(true)) + obj.Set("bar", String("foo")) + checkJson(t, obj, true, "{\"foo\":true,\"bar\":\"foo\"}", + "{\"bar\":\"foo\",\"foo\":true}") + checkJson(t, obj, false, "{\n \"foo\": true,\n \"bar\": \"foo\"\n}\n", + "{\n \"bar\": \"foo\",\n \"foo\": true\n}\n") +} + +func TestJsonEncodeNesting(t *testing.T) { + obj := Object() + arr1 := obj.Set("foo", Array()) + arr2 := arr1.Add(Array()) + arr1.Add(Long(3)) + arr2.Add(Long(5)) + arr2.Add(Long(7)) + checkJson(t, obj, true, "{\"foo\":[[5,7],3]}") + checkJson(t, obj, false, "{\n"+ + " \"foo\": [\n"+ + " [\n"+ + " 5,\n"+ + " 7\n"+ + " ],\n"+ + " 3\n"+ + " ]\n"+ + "}\n") +} + +type brokenWriter struct{} + +func (*brokenWriter) Write(p []byte) (n int, err error) { + return 0, errors.New("bad writer") +} + +func TestJsonEncodeErrorPropagation(t *testing.T) { + err := EncodeJson(Bool(true), true, &brokenWriter{}) + assert.Equal(t, "bad writer", err.Error()) +} + +func TestJsonDecodeNull(t *testing.T) { + verifyEmpty()(t, FromJson("null")) +} + +func TestJsonDecodeBool(t *testing.T) { + verifyBool(false)(t, FromJson("false")) + verifyBool(true)(t, FromJson("true")) +} + +func TestJsonDecodeNumber(t *testing.T) { + verifyLong(0)(t, FromJson("0")) + verifyLong(1)(t, FromJson("1")) + verifyLong(2)(t, FromJson("2")) + verifyLong(3)(t, FromJson("3")) + verifyLong(4)(t, FromJson("4")) + verifyLong(5)(t, FromJson("5")) + verifyLong(6)(t, FromJson("6")) + verifyLong(7)(t, FromJson("7")) + verifyLong(8)(t, FromJson("8")) + verifyLong(9)(t, FromJson("9")) + verifyLong(-9)(t, FromJson("-9")) + verifyDouble(5.5)(t, FromJson("5.5")) + verifyDouble(5e7)(t, FromJson("5e7")) + verifyLong(9223372036854775807)(t, FromJson("9223372036854775807")) +} + +func json_string(str string) string { + val := FromJson("\"" + str + "\"") + if !val.Valid() { + return "" + } + return val.AsString() +} + +func TestJsonDecodeString(t *testing.T) { + assert.Equal(t, "", json_string("")) + assert.Equal(t, "foo", json_string("foo")) + assert.Equal(t, "\"", json_string("\\\"")) + assert.Equal(t, "\b", json_string("\\b")) + assert.Equal(t, "\f", json_string("\\f")) + assert.Equal(t, "\n", json_string("\\n")) + assert.Equal(t, "\r", json_string("\\r")) + assert.Equal(t, "\t", json_string("\\t")) + assert.Equal(t, "A", json_string("\\u0041")) + assert.Equal(t, "\x0f", json_string("\\u000f")) + assert.Equal(t, "\x18", json_string("\\u0018")) + assert.Equal(t, "\x29", json_string("\\u0029")) + assert.Equal(t, "\x3a", json_string("\\u003a")) + assert.Equal(t, "\x4b", json_string("\\u004b")) + assert.Equal(t, "\x5c", json_string("\\u005c")) + assert.Equal(t, "\x6d", json_string("\\u006d")) + assert.Equal(t, "\x7e", json_string("\\u007e")) + assert.Equal(t, "\x7f", json_string("\\u007f")) + assert.Equal(t, "\xc2\x80", json_string("\\u0080")) + assert.Equal(t, "\xdf\xbf", json_string("\\u07ff")) + assert.Equal(t, "\xe0\xa0\x80", json_string("\\u0800")) + assert.Equal(t, "\xed\x9f\xbf", json_string("\\ud7ff")) + assert.Equal(t, "\xee\x80\x80", json_string("\\ue000")) + assert.Equal(t, "\xef\xbf\xbf", json_string("\\uffff")) + assert.Equal(t, "\xf0\x90\x80\x80", json_string("\\ud800\\udc00")) + assert.Equal(t, "\xf4\x8f\xbf\xbf", json_string("\\udbff\\udfff")) +} + +func TestJsonDecodeData(t *testing.T) { + verifyData([]byte{})(t, FromJson("x")) + verifyData([]byte{0, 0})(t, FromJson("x0000")) + verifyData([]byte{0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef})(t, FromJson("x1234567890abcdefABCDEF")) +} + +func TestJsonDecodeArray(t *testing.T) { + verifyArray([]verifyValue{})(t, FromJson("[]")) + verify := verifyArray([]verifyValue{ + verifyLong(123), + verifyDouble(0.5), + verifyString("foo"), + verifyBool(true), + }) + verify(t, FromJson("[123,0.5,\"foo\",true]")) +} + +func TestJsonDecodeObject(t *testing.T) { + verifyObject(map[string]verifyValue{})(t, FromJson("{}")) + verify := verifyObject(map[string]verifyValue{ + "a": verifyLong(123), + "b": verifyDouble(0.5), + "c": verifyString("foo"), + "d": verifyBool(true), + "e": verifyData([]byte{0xff, 0x00, 0x11}), + }) + verify(t, FromJson("{\"a\":123,\"b\":0.5,\"c\":\"foo\",\"d\":true,\"e\":xff0011}")) +} + +func TestJsonDecodeNested(t *testing.T) { + verify := verifyObject(map[string]verifyValue{ + "a": verifyObject(map[string]verifyValue{ + "b": verifyArray([]verifyValue{ + verifyArray([]verifyValue{ + verifyLong(1), + verifyLong(2), + verifyLong(3), + }), + }), + "c": verifyArray([]verifyValue{ + verifyArray([]verifyValue{ + verifyLong(4), + }), + }), + }), + }) + verify(t, FromJson("{\"a\":{\"b\":[[1,2,3]],\"c\":[[4]]}}")) +} + +func TestJsonDecodeWhitespace(t *testing.T) { + verifyFromJson("true")(t, FromJson("\n\r\t true")) + verifyFromJson("true")(t, FromJson(" true ")) + verifyFromJson("false")(t, FromJson(" false ")) + verifyFromJson("null")(t, FromJson(" null ")) + verifyFromJson("\"foo\"")(t, FromJson(" \"foo\" ")) + verifyFromJson("{}")(t, FromJson(" { } ")) + verifyFromJson("[]")(t, FromJson(" [ ] ")) + verifyFromJson("5")(t, FromJson(" 5 ")) + verifyFromJson("[1]")(t, FromJson(" [ 1 ] ")) + verifyFromJson("[1,2,3]")(t, FromJson(" [ 1 , 2 , 3 ] ")) + verifyFromJson("{\"a\":1}")(t, FromJson(" { \"a\" : 1 } ")) + verifyFromJson("{\"a\":{\"b\":[[1,2,3]],\"c\":[[4]]}}")(t, + FromJson(" { \"a\" : { \"b\" : [ [ 1 , 2 , 3 ] ] , \"c\" : [ [ 4 ] ] } } ")) +} + +func TestJsonDecodeInvalidInput(t *testing.T) { + verifyInvalid()(t, FromJson("")) + verifyInvalid()(t, FromJson("[")) + verifyInvalid()(t, FromJson("{")) + verifyInvalid()(t, FromJson("]")) + verifyInvalid()(t, FromJson("}")) + verifyInvalid()(t, FromJson("{]")) + verifyInvalid()(t, FromJson("[}")) + verifyInvalid()(t, FromJson("+5")) + verifyInvalid()(t, FromJson("fals")) + verifyInvalid()(t, FromJson("tru")) + verifyInvalid()(t, FromJson("nul")) + verifyInvalid()(t, FromJson("bar")) + verifyInvalid()(t, FromJson("\"bar")) + verifyInvalid()(t, FromJson("bar\"")) + verifyInvalid()(t, FromJson("'bar\"")) + verifyInvalid()(t, FromJson("\"bar'")) + verifyInvalid()(t, FromJson("{\"foo")) +} + +func TestJsonDecodeSimplifiedForm(t *testing.T) { + verifyFromJson("\"foo\"")(t, FromJson("'foo'")) + verifyFromJson("{\"a\":123,\"b\":0.5,\"c\":\"foo\",\"d\":true}")(t, FromJson("{a:123,b:0.5,c:'foo',d:true}")) + verifyFromJson("{\"a\":{\"b\":[[1,2,3]],\"c\":[[4]]}}")(t, FromJson("{a:{b:[[1,2,3]],c:[[4]]}}")) +} + +func TestJsonDecodeMultipleValues(t *testing.T) { + data := "true {} false [] null \"foo\" 'bar' 1.5 null" + input := strings.NewReader(data) + verifyFromJson("true")(t, DecodeJson(input)) + verifyFromJson("{}")(t, DecodeJson(input)) + verifyFromJson("false")(t, DecodeJson(input)) + verifyFromJson("[]")(t, DecodeJson(input)) + verifyFromJson("null")(t, DecodeJson(input)) + verifyFromJson("\"foo\"")(t, DecodeJson(input)) + verifyFromJson("\"bar\"")(t, DecodeJson(input)) + verifyFromJson("1.5")(t, DecodeJson(input)) + verifyFromJson("null")(t, DecodeJson(input)) + // Note that one extra byte is always looked at when parsing + // json. Since io.Reader does not support peek/unread this + // byte will be lost + bad_data := "true{}" + bad_input := strings.NewReader(bad_data) + verifyFromJson("true")(t, DecodeJson(bad_input)) + verifyInvalid()(t, DecodeJson(bad_input)) +} diff --git a/client/go/internal/vespa/slime/leaf_test.go b/client/go/internal/vespa/slime/leaf_test.go index 501af06c6620..25fc5c433190 100644 --- a/client/go/internal/vespa/slime/leaf_test.go +++ b/client/go/internal/vespa/slime/leaf_test.go @@ -22,6 +22,11 @@ func checkLeaf(t *testing.T, value Value, expect expectLeaf) { expect.dataVal = emptyBytes } assert.Equal(t, value.Valid(), !expect.invalid) + if expect.invalid { + assert.Equal(t, AsError(value).Error(), "invalid value") + } else { + assert.Equal(t, AsError(value), nil) + } assert.Equal(t, value.Type(), expect.mytype) assert.Equal(t, value.AsBool(), expect.boolVal) assert.Equal(t, value.AsLong(), expect.longVal) @@ -30,6 +35,12 @@ func checkLeaf(t *testing.T, value Value, expect expectLeaf) { assert.Equal(t, value.AsData(), expect.dataVal) } +func TestError(t *testing.T) { + err := ErrorMsg("test error") + assert.False(t, err.Valid()) + assert.Equal(t, AsError(err).Error(), "test error") +} + func TestEmpty(t *testing.T) { checkLeaf(t, Empty, expectLeaf{}) checkLeaf(t, Invalid, expectLeaf{invalid: true}) diff --git a/client/go/internal/vespa/slime/value.go b/client/go/internal/vespa/slime/value.go index f48143f4fc82..ecf9eefcbdb0 100644 --- a/client/go/internal/vespa/slime/value.go +++ b/client/go/internal/vespa/slime/value.go @@ -5,7 +5,6 @@ package slime var ( emptyBytes []byte = make([]byte, 0) Empty Value = &emptyValue{} - Invalid Value = (*emptyValue)(nil) ) type Value interface { @@ -28,7 +27,7 @@ type Value interface { type emptyValue struct{} -func (v *emptyValue) Valid() bool { return (v != nil) } +func (*emptyValue) Valid() bool { return true } func (*emptyValue) Type() Type { return EMPTY } func (*emptyValue) AsBool() bool { return false } func (*emptyValue) AsLong() int64 { return 0 } @@ -43,3 +42,5 @@ func (*emptyValue) Field(name string) Value { return Invalid } func (*emptyValue) EachField(func(name string, value Value)) {} func (*emptyValue) Add(value Value) Value { return Invalid } func (*emptyValue) Set(name string, value Value) Value { return Invalid } + +func ToString(value Value) string { return ToJson(value, false) }