summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Shumaker <lukeshu@lukeshu.com>2023-01-30 21:54:38 -0700
committerLuke Shumaker <lukeshu@lukeshu.com>2023-01-30 21:54:38 -0700
commit8467bdaa181257d031a258a05012dc85adbcb233 (patch)
treebc9bf437a34905f0b7249352043aff9e9d80ebe8
parent0b57145421e7e4f165f64e73ee7c5d8102945569 (diff)
parent2e48a42fb9b9e946958810cfbb90ae85bee997e4 (diff)
Merge branch 'lukeshu/quality2'
-rw-r--r--.editorconfig27
-rw-r--r--README.md2
-rw-r--r--ReleaseNotes.md35
-rw-r--r--common.go16
-rw-r--r--compat/json/compat.go2
-rw-r--r--decode.go140
-rw-r--r--decode_scan.go34
-rw-r--r--decode_scan_test.go6
-rw-r--r--encode.go41
-rw-r--r--encode_escape.go (renamed from misc.go)108
-rw-r--r--encode_string.go111
-rw-r--r--errors.go5
-rw-r--r--internal/encode.go (renamed from internal/export_tags.go)4
-rw-r--r--internal/parse.go50
-rw-r--r--internal/parse_test.go78
-rw-r--r--internal/tags.go7
-rw-r--r--ioutil.go31
-rw-r--r--methods_test.go198
-rw-r--r--struct.go4
-rw-r--r--test_export.go18
20 files changed, 668 insertions, 249 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..85ac8bf
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,27 @@
+root = true
+
+[*]
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.go]
+indent_style = tab
+
+[{go.mod,go.sum}]
+indent_style = tab
+
+[Makefile]
+; If somehow this gets set to not-tab, then the resulting Makefile
+; won't work.
+indent_style = tab
+
+[*.md]
+; Emacs markdown-mode gets mixed tabs/spaces wrong, and so I have zero
+; faith that any other tool gets it right.
+indent_style = space
+
+[*.yml]
+indent_style = space
+indent_size = 2
diff --git a/README.md b/README.md
index 955258c..fcb46fa 100644
--- a/README.md
+++ b/README.md
@@ -123,7 +123,7 @@ common use of it will be
```go
lowmemjson.NewEncoder(&lowmemjson.ReEncoder{
- Out: out,
+ Out: out,
// settings here
}).Encode(val)
```
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
new file mode 100644
index 0000000..a2365f0
--- /dev/null
+++ b/ReleaseNotes.md
@@ -0,0 +1,35 @@
+# v0.2.1 (TBD)
+
+ Theme: Code quality
+
+ This release improves code quality; getting various linters to pass,
+ adding tests (and a few bug-fixes), refactoring things to be
+ clearer, fixing some mistakes in the documentation.
+
+ User-facing changes:
+
+ - Encoder: `*EncodeMethodError` is now also used when a method
+ produces invalid JSON.
+ - Decoder: The offset in `*DecodeTypeError`s now correctly point
+ the start of the value, rather than somewhere in the middle of
+ it.
+
+# v0.2.0 (2023-01-26)
+
+ Theme: Add documentation
+
+ This release doesn't make any major changes, and is just adding
+ documentation. I have removed a few minor things that I didn't want
+ to write documentation for.
+
+ Breaking changes:
+
+ - Drop the following shorthand functions:
+ + `func Decode(r io.RuneScanner, ptr any) error { return NewDecoder(r).Decode(ptr) }`
+ + `func DecodeThenEOF(r io.RuneScanner, ptr any) error { return NewDecoder(r).DecodeThenEOF(ptr) }`
+ + `func Encode(w io.Writer, obj any) (err error) { return NewEncoder(w).Encode(obj) }`
+ - Drop `const Tab = "\t"`.
+
+# v0.1.0 (2022-09-19)
+
+ Theme: Initial release
diff --git a/common.go b/common.go
new file mode 100644
index 0000000..90156b9
--- /dev/null
+++ b/common.go
@@ -0,0 +1,16 @@
+// Copyright (C) 2022-2023 Luke Shumaker <lukeshu@lukeshu.com>
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package lowmemjson
+
+import (
+ "encoding/json"
+ "reflect"
+)
+
+var (
+ numberType = reflect.TypeOf(json.Number(""))
+ byteType = reflect.TypeOf(byte(0))
+ byteSliceType = reflect.TypeOf(([]byte)(nil))
+)
diff --git a/compat/json/compat.go b/compat/json/compat.go
index 0c9e800..48d708b 100644
--- a/compat/json/compat.go
+++ b/compat/json/compat.go
@@ -183,7 +183,7 @@ func convertDecodeError(err error) error {
default:
err = &SyntaxError{
msg: terr.Err.Error(),
- Offset: terr.Offset,
+ Offset: terr.Offset + 1,
}
}
case *lowmemjson.DecodeTypeError:
diff --git a/decode.go b/decode.go
index f351037..7ae723c 100644
--- a/decode.go
+++ b/decode.go
@@ -90,8 +90,9 @@ type Decoder struct {
useNumber bool
// state
- err error
- stack []decodeStackItem
+ err error
+ posStack []int64
+ structStack []decodeStackItem
}
const maxNestingDepth = 10000
@@ -150,40 +151,48 @@ func (dec *Decoder) More() bool {
return e == nil && t != internal.RuneTypeEOF
}
-func (dec *Decoder) stackPush(par reflect.Type, idx any) {
- dec.stack = append(dec.stack, decodeStackItem{par, idx})
+func (dec *Decoder) posStackPush() {
+ dec.posStack = append(dec.posStack, dec.InputOffset())
}
-func (dec *Decoder) stackPop() {
- dec.stack = dec.stack[:len(dec.stack)-1]
+func (dec *Decoder) posStackPop() {
+ dec.posStack = dec.posStack[:len(dec.posStack)-1]
}
-func (dec *Decoder) stackStr() string {
+func (dec *Decoder) structStackPush(par reflect.Type, idx any) {
+ dec.structStack = append(dec.structStack, decodeStackItem{par, idx})
+}
+
+func (dec *Decoder) structStackPop() {
+ dec.structStack = dec.structStack[:len(dec.structStack)-1]
+}
+
+func (dec *Decoder) structStackStr() string {
var buf strings.Builder
buf.WriteString("v")
- for _, item := range dec.stack {
+ for _, item := range dec.structStack {
fmt.Fprintf(&buf, "[%#v]", item.idx)
}
return buf.String()
}
-func (dec *Decoder) stackParent() string {
- last := len(dec.stack) - 1
- if last > 0 && dec.stack[last].par.Kind() != reflect.Struct && dec.stack[last-1].par.Kind() == reflect.Struct {
+func (dec *Decoder) structStackParent() string {
+ last := len(dec.structStack) - 1
+ if last > 0 && dec.structStack[last].par.Kind() != reflect.Struct && dec.structStack[last-1].par.Kind() == reflect.Struct {
last--
}
- if last >= 0 && dec.stack[last].par.Kind() == reflect.Struct {
- return dec.stack[last].par.Name()
+ if last >= 0 && dec.structStack[last].par.Kind() == reflect.Struct {
+ return dec.structStack[last].par.Name()
}
return ""
}
-func (dec *Decoder) stackName() string {
- if dec.stackParent() == "" {
+func (dec *Decoder) structStackName() string {
+ if dec.structStackParent() == "" {
return ""
}
var fields []string
- for _, elem := range dec.stack {
+ for _, elem := range dec.structStack {
if elem.par.Kind() == reflect.Struct {
fields = append(fields, elem.idx.(string))
}
@@ -259,14 +268,14 @@ type decodeError DecodeError
func (dec *Decoder) panicType(jTyp string, gTyp reflect.Type, err error) {
panic(decodeError{
- Field: dec.stackStr(),
- FieldParent: dec.stackParent(),
- FieldName: dec.stackName(),
+ Field: dec.structStackStr(),
+ FieldParent: dec.structStackParent(),
+ FieldName: dec.structStackName(),
Err: &DecodeTypeError{
GoType: gTyp,
JSONType: jTyp,
Err: err,
- Offset: dec.InputOffset(),
+ Offset: dec.posStack[len(dec.posStack)-1],
},
})
}
@@ -275,9 +284,9 @@ func (dec *Decoder) readRune() (rune, internal.RuneType) {
c, _, t, e := dec.io.ReadRuneType()
if e != nil {
panic(decodeError{
- Field: dec.stackStr(),
- FieldParent: dec.stackParent(),
- FieldName: dec.stackName(),
+ Field: dec.structStackStr(),
+ FieldParent: dec.structStackParent(),
+ FieldName: dec.structStackName(),
Err: e,
})
}
@@ -320,9 +329,9 @@ func (sc *decRuneTypeScanner) ReadRuneType() (rune, int, internal.RuneType, erro
c, s, t, e := sc.dec.io.ReadRuneType()
if e != nil {
panic(decodeError{
- Field: sc.dec.stackStr(),
- FieldParent: sc.dec.stackParent(),
- FieldName: sc.dec.stackName(),
+ Field: sc.dec.structStackStr(),
+ FieldParent: sc.dec.structStackParent(),
+ FieldName: sc.dec.structStackName(),
Err: e,
})
}
@@ -381,6 +390,8 @@ var kind2bits = map[reflect.Kind]int{
}
func (dec *Decoder) decode(val reflect.Value, nullOK bool) {
+ dec.posStackPush()
+ defer dec.posStackPop()
typ := val.Type()
switch {
case val.CanAddr() && reflect.PointerTo(typ) == rawMessagePtrType:
@@ -388,17 +399,17 @@ func (dec *Decoder) decode(val reflect.Value, nullOK bool) {
var buf bytes.Buffer
dec.scan(&buf)
if err := val.Addr().Interface().(*json.RawMessage).UnmarshalJSON(buf.Bytes()); err != nil {
- dec.panicType(t.JSONType(), typ, err)
+ dec.panicType(t.JSONType(), reflect.PointerTo(typ), err)
}
case val.CanAddr() && reflect.PointerTo(typ).Implements(decodableType):
t := dec.peekRuneType()
obj := val.Addr().Interface().(Decodable)
l := dec.limitingScanner()
if err := obj.DecodeJSON(l); err != nil {
- dec.panicType(t.JSONType(), typ, err)
+ dec.panicType(t.JSONType(), reflect.PointerTo(typ), err)
}
if _, _, err := l.ReadRune(); err != io.EOF {
- dec.panicType(t.JSONType(), typ, fmt.Errorf("did not consume entire %s", t.JSONType()))
+ dec.panicType(t.JSONType(), reflect.PointerTo(typ), fmt.Errorf("did not consume entire %s", t.JSONType()))
}
case val.CanAddr() && reflect.PointerTo(typ).Implements(jsonUnmarshalerType):
t := dec.peekRuneType()
@@ -406,7 +417,7 @@ func (dec *Decoder) decode(val reflect.Value, nullOK bool) {
dec.scan(&buf)
obj := val.Addr().Interface().(json.Unmarshaler)
if err := obj.UnmarshalJSON(buf.Bytes()); err != nil {
- dec.panicType(t.JSONType(), typ, err)
+ dec.panicType(t.JSONType(), reflect.PointerTo(typ), err)
}
case val.CanAddr() && reflect.PointerTo(typ).Implements(textUnmarshalerType):
if nullOK && dec.peekRuneType() == internal.RuneTypeNullN {
@@ -530,12 +541,16 @@ func (dec *Decoder) decode(val reflect.Value, nullOK bool) {
index := indexStruct(typ)
var nameBuf strings.Builder
dec.decodeObject(typ, func() {
+ dec.posStackPush()
+ defer dec.posStackPop()
nameBuf.Reset()
dec.decodeString(nil, &nameBuf)
}, func() {
+ dec.posStackPush()
+ defer dec.posStackPop()
name := nameBuf.String()
- dec.stackPush(typ, name)
- defer dec.stackPop()
+ dec.structStackPush(typ, name)
+ defer dec.structStackPop()
idx, ok := index.byName[name]
if !ok {
for oidx := range index.byPos {
@@ -613,17 +628,19 @@ func (dec *Decoder) decode(val reflect.Value, nullOK bool) {
val.Set(reflect.MakeMap(typ))
}
var nameBuf bytes.Buffer
+ var nameValPtr reflect.Value
dec.decodeObject(typ, func() {
+ dec.posStackPush()
+ defer dec.posStackPop()
nameBuf.Reset()
dec.decodeString(nil, &nameBuf)
- }, func() {
nameValTyp := typ.Key()
- nameValPtr := reflect.New(nameValTyp)
+ nameValPtr = reflect.New(nameValTyp)
switch {
case reflect.PointerTo(nameValTyp).Implements(textUnmarshalerType):
obj := nameValPtr.Interface().(encoding.TextUnmarshaler)
if err := obj.UnmarshalText(nameBuf.Bytes()); err != nil {
- dec.panicType("string", nameValTyp, err)
+ dec.panicType("string", reflect.PointerTo(nameValTyp), err)
}
default:
switch nameValTyp.Kind() {
@@ -645,8 +662,11 @@ func (dec *Decoder) decode(val reflect.Value, nullOK bool) {
dec.panicType("object", typ, &DecodeArgumentError{Type: nameValTyp})
}
}
- dec.stackPush(typ, nameValPtr.Elem())
- defer dec.stackPop()
+ }, func() {
+ dec.posStackPush()
+ defer dec.posStackPop()
+ dec.structStackPush(typ, nameValPtr.Elem())
+ defer dec.structStackPop()
fValPtr := reflect.New(typ.Elem())
dec.decode(fValPtr.Elem(), false)
@@ -699,8 +719,10 @@ func (dec *Decoder) decode(val reflect.Value, nullOK bool) {
}
i := 0
dec.decodeArray(typ, func() {
- dec.stackPush(typ, i)
- defer dec.stackPop()
+ dec.posStackPush()
+ defer dec.posStackPop()
+ dec.structStackPush(typ, i)
+ defer dec.structStackPop()
mValPtr := reflect.New(typ.Elem())
dec.decode(mValPtr.Elem(), false)
val.Set(reflect.Append(val, mValPtr.Elem()))
@@ -718,8 +740,10 @@ func (dec *Decoder) decode(val reflect.Value, nullOK bool) {
i := 0
n := val.Len()
dec.decodeArray(typ, func() {
- dec.stackPush(typ, i)
- defer dec.stackPop()
+ dec.posStackPush()
+ defer dec.posStackPop()
+ dec.structStackPush(typ, i)
+ defer dec.structStackPop()
if i < n {
mValPtr := reflect.New(typ.Elem())
dec.decode(mValPtr.Elem(), false)
@@ -776,12 +800,16 @@ func (dec *Decoder) decodeAny() any {
typ := reflect.TypeOf(ret)
var nameBuf strings.Builder
dec.decodeObject(typ, func() {
+ dec.posStackPush()
+ defer dec.posStackPop()
nameBuf.Reset()
dec.decodeString(nil, &nameBuf)
}, func() {
+ dec.posStackPush()
+ defer dec.posStackPop()
name := nameBuf.String()
- dec.stackPush(typ, name)
- defer dec.stackPop()
+ dec.structStackPush(typ, name)
+ defer dec.structStackPop()
ret[name] = dec.decodeAny()
})
return ret
@@ -789,8 +817,10 @@ func (dec *Decoder) decodeAny() any {
ret := []any{}
typ := reflect.TypeOf(ret)
dec.decodeArray(typ, func() {
- dec.stackPush(typ, len(ret))
- defer dec.stackPop()
+ dec.posStackPush()
+ defer dec.posStackPop()
+ dec.structStackPush(typ, len(ret))
+ defer dec.structStackPop()
ret = append(ret, dec.decodeAny())
})
return ret
@@ -840,27 +870,37 @@ func DecodeObject(r io.RuneScanner, decodeKey, decodeVal func(io.RuneScanner) er
}
}()
dec := NewDecoder(r)
+ dec.posStackPush()
+ defer dec.posStackPop()
dec.decodeObject(nil,
func() {
+ dec.posStackPush()
+ defer dec.posStackPop()
l := dec.limitingScanner()
if err := decodeKey(l); err != nil {
+ // TODO: Find a better Go type to use than `nil`.
dec.panicType("string", nil, err)
}
if _, _, err := l.ReadRune(); err != io.EOF {
+ // TODO: Find a better Go type to use than `nil`.
dec.panicType("string", nil, fmt.Errorf("did not consume entire string"))
}
},
func() {
+ dec.posStackPush()
+ defer dec.posStackPop()
t := dec.peekRuneType()
l := dec.limitingScanner()
if err := decodeVal(l); err != nil {
+ // TODO: Find a better Go type to use than `nil`.
dec.panicType(t.JSONType(), nil, err)
}
if _, _, err := l.ReadRune(); err != io.EOF {
+ // TODO: Find a better Go type to use than `nil`.
dec.panicType(t.JSONType(), nil, fmt.Errorf("did not consume entire %s", t.JSONType()))
}
})
- return err
+ return nil
}
func (dec *Decoder) decodeObject(gTyp reflect.Type, decodeKey, decodeVal func()) {
@@ -910,17 +950,23 @@ func DecodeArray(r io.RuneScanner, decodeMember func(r io.RuneScanner) error) (e
}
}()
dec := NewDecoder(r)
+ dec.posStackPush()
+ defer dec.posStackPop()
dec.decodeArray(nil, func() {
+ dec.posStackPush()
+ defer dec.posStackPop()
t := dec.peekRuneType()
l := dec.limitingScanner()
if err := decodeMember(l); err != nil {
+ // TODO: Find a better Go type to use than `nil`.
dec.panicType(t.JSONType(), nil, err)
}
if _, _, err := l.ReadRune(); err != io.EOF {
+ // TODO: Find a better Go type to use than `nil`.
dec.panicType(t.JSONType(), nil, fmt.Errorf("did not consume entire %s", t.JSONType()))
}
})
- return
+ return nil
}
func (dec *Decoder) decodeArray(gTyp reflect.Type, decodeMember func()) {
diff --git a/decode_scan.go b/decode_scan.go
index 249975d..387fcea 100644
--- a/decode_scan.go
+++ b/decode_scan.go
@@ -35,34 +35,30 @@ type runeTypeScannerImpl struct {
inner io.RuneScanner // initialized by constructor
- initialized bool
-
parser internal.Parser // initialized by constructor
offset int64
- repeat bool
- stuck bool
- rRune rune
- rRuneOK bool
- rSize int
- rType internal.RuneType
- rErr error
+ initialized bool
+ repeat bool
+
+ rRune rune
+ rSize int
+ rType internal.RuneType
+ rErr error
}
var _ runeTypeScanner = (*runeTypeScannerImpl)(nil)
func (sc *runeTypeScannerImpl) Reset() {
sc.parser.Reset()
- unread := sc.stuck && sc.rType == internal.RuneTypeEOF && sc.rRuneOK
- sc.stuck = false
- sc.repeat = false
- if unread {
+ if sc.repeat || (sc.rType == internal.RuneTypeEOF && sc.rSize > 0) {
+ sc.repeat = false
// re-figure the rType and rErr
var err error
sc.rType, err = sc.parser.HandleRune(sc.rRune)
if err != nil {
sc.rErr = &DecodeSyntaxError{
- Offset: sc.offset,
+ Offset: sc.offset - int64(sc.rSize),
Err: err,
}
} else {
@@ -75,21 +71,21 @@ func (sc *runeTypeScannerImpl) Reset() {
func (sc *runeTypeScannerImpl) ReadRuneType() (rune, int, internal.RuneType, error) {
switch {
- case sc.stuck:
+ case sc.initialized && (sc.rType == internal.RuneTypeError || sc.rType == internal.RuneTypeEOF):
// do nothing
case sc.repeat:
_, _, _ = sc.inner.ReadRune()
default:
+ sc.initialized = true
var err error
sc.rRune, sc.rSize, err = sc.inner.ReadRune()
sc.offset += int64(sc.rSize)
- sc.rRuneOK = err == nil
switch err {
case nil:
sc.rType, err = sc.parser.HandleRune(sc.rRune)
if err != nil {
sc.rErr = &DecodeSyntaxError{
- Offset: sc.offset,
+ Offset: sc.offset - int64(sc.rSize),
Err: err,
}
} else {
@@ -113,9 +109,7 @@ func (sc *runeTypeScannerImpl) ReadRuneType() (rune, int, internal.RuneType, err
}
}
}
- sc.initialized = true
sc.repeat = false
- sc.stuck = sc.rType == internal.RuneTypeEOF || sc.rType == internal.RuneTypeError
return sc.rRune, sc.rSize, sc.rType, sc.rErr
}
@@ -137,7 +131,7 @@ func (sc *runeTypeScannerImpl) ReadRune() (rune, int, error) {
// unread, or if that call returned a rune with size 0, then
// ErrInvalidUnreadRune is returned. Otherwise, nil is returned.
func (sc *runeTypeScannerImpl) UnreadRune() error {
- if !sc.initialized || sc.repeat || sc.rSize == 0 {
+ if sc.repeat || sc.rSize == 0 {
return ErrInvalidUnreadRune
}
sc.repeat = true
diff --git a/decode_scan_test.go b/decode_scan_test.go
index f5ceee0..6a430ab 100644
--- a/decode_scan_test.go
+++ b/decode_scan_test.go
@@ -141,9 +141,9 @@ func TestRuneTypeScanner(t *testing.T) {
{'[', 1, internal.RuneTypeArrayBeg, nil},
{'0', 1, internal.RuneTypeNumberIntZero, nil},
{',', 1, internal.RuneTypeArrayComma, nil},
- {']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 5, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}},
- {']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 5, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}},
- {']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 5, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}},
+ {']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 4, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}},
+ {']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 4, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}},
+ {']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 4, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}},
}},
"multi-value": {`1{}`, `}`, []ReadRuneTypeResult{
{'1', 1, internal.RuneTypeNumberIntDig, nil},
diff --git a/encode.go b/encode.go
index 41032e5..00848ed 100644
--- a/encode.go
+++ b/encode.go
@@ -17,7 +17,6 @@ import (
"sort"
"strconv"
"strings"
- "unicode/utf8"
"unsafe"
)
@@ -147,7 +146,11 @@ func encode(w io.Writer, val reflect.Value, escaper BackslashEscaper, quote bool
}})
}
if err := validator.Close(); err != nil && !errors.Is(err, iofs.ErrClosed) {
- panic(encodeError{err})
+ panic(encodeError{&EncodeMethodError{
+ Type: val.Type(),
+ SourceFunc: "EncodeJSON",
+ Err: err,
+ }})
}
case val.Kind() != reflect.Pointer && val.CanAddr() && reflect.PointerTo(val.Type()).Implements(jsonMarshalerType):
@@ -174,10 +177,18 @@ func encode(w io.Writer, val reflect.Value, escaper BackslashEscaper, quote bool
// Use a sub-ReEncoder to check that it's a full element.
validator := &ReEncoder{Out: w, BackslashEscape: escaper}
if _, err := validator.Write(dat); err != nil {
- panic(encodeError{err})
+ panic(encodeError{&EncodeMethodError{
+ Type: val.Type(),
+ SourceFunc: "MarshalJSON",
+ Err: err,
+ }})
}
if err := validator.Close(); err != nil {
- panic(encodeError{err})
+ panic(encodeError{&EncodeMethodError{
+ Type: val.Type(),
+ SourceFunc: "MarshalJSON",
+ Err: err,
+ }})
}
case val.Kind() != reflect.Pointer && val.CanAddr() && reflect.PointerTo(val.Type()).Implements(textMarshalerType):
@@ -426,28 +437,6 @@ func encode(w io.Writer, val reflect.Value, escaper BackslashEscaper, quote bool
}
}
-func encodeStringFromString(w io.Writer, escaper BackslashEscaper, str string) {
- encodeWriteByte(w, '"')
- for _, c := range str {
- if _, err := writeStringChar(w, c, BackslashEscapeNone, escaper); err != nil {
- panic(encodeError{err})
- }
- }
- encodeWriteByte(w, '"')
-}
-
-func encodeStringFromBytes(w io.Writer, escaper BackslashEscaper, str []byte) {
- encodeWriteByte(w, '"')
- for i := 0; i < len(str); {
- c, size := utf8.DecodeRune(str[i:])
- if _, err := writeStringChar(w, c, BackslashEscapeNone, escaper); err != nil {
- panic(encodeError{err})
- }
- i += size
- }
- encodeWriteByte(w, '"')
-}
-
func encodeArray(w io.Writer, val reflect.Value, escaper BackslashEscaper, cycleDepth uint, cycleSeen map[any]struct{}) {
encodeWriteByte(w, '[')
n := val.Len()
diff --git a/misc.go b/encode_escape.go
index fb96b4e..ab0d9c1 100644
--- a/misc.go
+++ b/encode_escape.go
@@ -5,45 +5,9 @@
package lowmemjson
import (
- "encoding/json"
- "io"
- "reflect"
"unicode/utf8"
-
- "git.lukeshu.com/go/lowmemjson/internal"
-)
-
-var (
- numberType = reflect.TypeOf(json.Number(""))
- byteType = reflect.TypeOf(byte(0))
- byteSliceType = reflect.TypeOf(([]byte)(nil))
)
-// generic I/O /////////////////////////////////////////////////////////////////
-
-func writeByte(w io.Writer, c byte) error {
- if br, ok := w.(interface{ WriteByte(byte) error }); ok {
- return br.WriteByte(c)
- }
- var buf [1]byte
- buf[0] = c
- if _, err := w.Write(buf[:]); err != nil {
- return err
- }
- return nil
-}
-
-func writeRune(w io.Writer, c rune) (int, error) {
- if rw, ok := w.(interface{ WriteRune(rune) (int, error) }); ok {
- return rw.WriteRune(c)
- }
- var buf [utf8.UTFMax]byte
- n := utf8.EncodeRune(buf[:], c)
- return w.Write(buf[:n])
-}
-
-// JSON string encoding ////////////////////////////////////////////////////////
-
// BackslashEscapeMode identifies one of the three ways that a
// character may be represented in a JSON string:
//
@@ -137,75 +101,3 @@ func EscapeDefaultNonHTMLSafe(c rune, wasEscaped BackslashEscapeMode) BackslashE
return EscapeJSSafe(c, wasEscaped)
}
}
-
-func writeStringUnicodeEscape(w io.Writer, c rune) (int, error) {
- buf := [6]byte{
- '\\',
- 'u',
- internal.Hex[(c>>12)&0xf],
- internal.Hex[(c>>8)&0xf],
- internal.Hex[(c>>4)&0xf],
- internal.Hex[(c>>0)&0xf],
- }
- return w.Write(buf[:])
-}
-
-func writeStringShortEscape(w io.Writer, c rune) (int, error) {
- var b byte
- switch c {
- case '"', '\\', '/':
- b = byte(c)
- case '\b':
- b = 'b'
- case '\f':
- b = 'f'
- case '\n':
- b = 'n'
- case '\r':
- b = 'r'
- case '\t':
- b = 't'
- default:
- panic("should not happen")
- }
- buf := [2]byte{'\\', b}
- return w.Write(buf[:])
-}
-
-func writeStringChar(w io.Writer, c rune, wasEscaped BackslashEscapeMode, escaper BackslashEscaper) (int, error) {
- if escaper == nil {
- escaper = EscapeDefault
- }
- switch escaper(c, wasEscaped) {
- case BackslashEscapeNone:
- switch {
- case c < 0x0020: // override, gotta escape these
- switch c {
- case '\b', '\f', '\n', '\r', '\t': // short-escape if possible
- return writeStringShortEscape(w, c)
- default:
- return writeStringUnicodeEscape(w, c)
- }
- case c == '"' || c == '\\': // override, gotta escape these
- return writeStringShortEscape(w, c)
- default: // obey
- return writeRune(w, c)
- }
- case BackslashEscapeShort:
- switch c {
- case '"', '\\', '/', '\b', '\f', '\n', '\r', '\t': // obey
- return writeStringShortEscape(w, c)
- default: // override, can't short-escape these
- return writeRune(w, c)
- }
- case BackslashEscapeUnicode:
- switch {
- case c > 0xFFFF: // override, can't escape these (TODO: unless we use UTF-16 surrogates?)
- return writeRune(w, c)
- default: // obey
- return writeStringUnicodeEscape(w, c)
- }
- default:
- panic("escaper returned an invalid escape mode")
- }
-}
diff --git a/encode_string.go b/encode_string.go
new file mode 100644
index 0000000..c5cb442
--- /dev/null
+++ b/encode_string.go
@@ -0,0 +1,111 @@
+// Copyright (C) 2022-2023 Luke Shumaker <lukeshu@lukeshu.com>
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package lowmemjson
+
+import (
+ "io"
+ "unicode/utf8"
+
+ "git.lukeshu.com/go/lowmemjson/internal"
+)
+
+func writeStringUnicodeEscape(w io.Writer, c rune) (int, error) {
+ buf := [6]byte{
+ '\\',
+ 'u',
+ internal.Hex[(c>>12)&0xf],
+ internal.Hex[(c>>8)&0xf],
+ internal.Hex[(c>>4)&0xf],
+ internal.Hex[(c>>0)&0xf],
+ }
+ return w.Write(buf[:])
+}
+
+func writeStringShortEscape(w io.Writer, c rune) (int, error) {
+ var b byte
+ switch c {
+ case '"', '\\', '/':
+ b = byte(c)
+ case '\b':
+ b = 'b'
+ case '\f':
+ b = 'f'
+ case '\n':
+ b = 'n'
+ case '\r':
+ b = 'r'
+ case '\t':
+ b = 't'
+ default:
+ panic("should not happen")
+ }
+ buf := [2]byte{'\\', b}
+ return w.Write(buf[:])
+}
+
+func writeStringChar(w io.Writer, c rune, wasEscaped BackslashEscapeMode, escaper BackslashEscaper) (int, error) {
+ if escaper == nil {
+ escaper = EscapeDefault
+ }
+ switch escaper(c, wasEscaped) {
+ case BackslashEscapeNone:
+ switch {
+ case c < 0x0020: // override, gotta escape these
+ switch c {
+ case '\b', '\f', '\n', '\r', '\t': // short-escape if possible
+ return writeStringShortEscape(w, c)
+ default:
+ return writeStringUnicodeEscape(w, c)
+ }
+ case c == '"' || c == '\\': // override, gotta escape these
+ return writeStringShortEscape(w, c)
+ default: // obey
+ return writeRune(w, c)
+ }
+ case BackslashEscapeShort:
+ switch c {
+ case '"', '\\', '/', '\b', '\f', '\n', '\r', '\t': // obey
+ return writeStringShortEscape(w, c)
+ default: // override, can't short-escape these
+ return writeRune(w, c)
+ }
+ case BackslashEscapeUnicode:
+ switch {
+ case c > 0xFFFF: // override, can't escape these (TODO: unless we use UTF-16 surrogates?)
+ return writeRune(w, c)
+ default: // obey
+ return writeStringUnicodeEscape(w, c)
+ }
+ default:
+ panic("escaper returned an invalid escape mode")
+ }
+}
+
+func encodeStringFromString(w io.Writer, escaper BackslashEscaper, str string) {
+ encodeWriteByte(w, '"')
+ for _, c := range str {
+ if _, err := writeStringChar(w, c, BackslashEscapeNone, escaper); err != nil {
+ panic(encodeError{err})
+ }
+ }
+ encodeWriteByte(w, '"')
+}
+
+func encodeStringFromBytes(w io.Writer, escaper BackslashEscaper, str []byte) {
+ encodeWriteByte(w, '"')
+ for i := 0; i < len(str); {
+ c, size := utf8.DecodeRune(str[i:])
+ if _, err := writeStringChar(w, c, BackslashEscapeNone, escaper); err != nil {
+ panic(encodeError{err})
+ }
+ i += size
+ }
+ encodeWriteByte(w, '"')
+}
+
+func init() {
+ internal.EncodeStringFromString = func(w io.Writer, s string) { encodeStringFromString(w, nil, s) }
+ internal.EncodeStringFromBytes = func(w io.Writer, s []byte) { encodeStringFromBytes(w, nil, s) }
+}
diff --git a/errors.go b/errors.go
index d36fc83..fe48723 100644
--- a/errors.go
+++ b/errors.go
@@ -138,8 +138,9 @@ type EncodeTypeError = json.UnsupportedTypeError
// }
type EncodeValueError = json.UnsupportedValueError
-// An EncodeMethodError wraps an error that is returned from an
-// object's method when encoding that object to JSON.
+// An EncodeMethodError either wraps an error that is returned from an
+// object's method when encoding that object to JSON, or wraps a
+// *ReEncodeSyntaxError for the method's output.
type EncodeMethodError struct {
Type reflect.Type // The Go type that the method is on
SourceFunc string // The method: "EncodeJSON", "MarshalJSON", or "MarshalText"
diff --git a/internal/export_tags.go b/internal/encode.go
index d8cf622..8aae673 100644
--- a/internal/export_tags.go
+++ b/internal/encode.go
@@ -1,4 +1,4 @@
-// Copyright (C) 2022 Luke Shumaker <lukeshu@lukeshu.com>
+// Copyright (C) 2022-2023 Luke Shumaker <lukeshu@lukeshu.com>
//
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -8,8 +8,6 @@ import (
"io"
)
-var ParseTag = parseTag
-
var (
EncodeStringFromBytes func(io.Writer, []byte)
EncodeStringFromString func(io.Writer, string)
diff --git a/internal/parse.go b/internal/parse.go
index bb849e7..b11aae6 100644
--- a/internal/parse.go
+++ b/internal/parse.go
@@ -264,10 +264,11 @@ type Parser struct {
//
// [ array: waiting for item to start or ']'
// a array: reading item / waiting for ',' or ']'
- // ] array: waiting for item to start
//
// Within each element type, the stack item is replaced, not pushed.
//
+ // (Keep each of these examples in-sync with parse_test.go.)
+ //
// For example, given the input string
//
// {"x":"y","a":"b"}
@@ -293,9 +294,34 @@ type Parser struct {
// o" {"x":"y","a":"b
// o {"x":"y","a":"b"
// {"x":"y","a":"b"}
+ //
+ // Or, given the input string
+ //
+ // ["x","y"]
+ //
+ // The stack would be
+ //
+ // stack processed
+ // ?
+ // [ [
+ // a" ["
+ // a" ["x
+ // a ["x"
+ // a? ["x",
+ // a" ["x","
+ // a" ["x","y
+ // a ["x","y"
+ // ["x","y"]
stack []RuneType
}
+func (par *Parser) init() {
+ if !par.initialized {
+ par.initialized = true
+ par.pushState(runeTypeAny)
+ }
+}
+
func (par *Parser) pushState(state RuneType) RuneType {
par.stack = append(par.stack, state)
return state
@@ -311,6 +337,7 @@ func (par *Parser) popState() {
}
func (par *Parser) stackString() string {
+ par.init()
var buf strings.Builder
for _, s := range par.stack {
buf.WriteString(s.String())
@@ -351,10 +378,7 @@ func (par *Parser) HandleEOF() (RuneType, error) {
if par.err != nil {
return RuneTypeError, par.err
}
- if !par.initialized {
- par.initialized = true
- par.pushState(runeTypeAny)
- }
+ par.init()
switch len(par.stack) {
case 0:
return RuneTypeEOF, nil
@@ -395,10 +419,7 @@ func (par *Parser) HandleRune(c rune) (RuneType, error) {
if par.err != nil {
return RuneTypeError, par.err
}
- if !par.initialized {
- par.initialized = true
- par.pushState(runeTypeAny)
- }
+ par.init()
if len(par.stack) == 0 {
switch c {
case 0x0020, 0x000A, 0x000D, 0x0009:
@@ -491,21 +512,12 @@ func (par *Parser) HandleRune(c rune) (RuneType, error) {
par.pushState(runeTypeAny)
return par.HandleRune(c)
}
- case RuneTypeArrayEnd: // waiting for item
- switch c {
- case 0x0020, 0x000A, 0x000D, 0x0009:
- return RuneTypeSpace, nil
- default:
- par.replaceState(RuneTypeArrayComma)
- par.pushState(runeTypeAny)
- return par.HandleRune(c)
- }
case RuneTypeArrayComma: // waiting for ',' or ']'
switch c {
case 0x0020, 0x000A, 0x000D, 0x0009:
return RuneTypeSpace, nil
case ',':
- par.replaceState(RuneTypeArrayEnd)
+ par.pushState(runeTypeAny)
return RuneTypeArrayComma, nil
case ']':
par.popState()
diff --git a/internal/parse_test.go b/internal/parse_test.go
new file mode 100644
index 0000000..34977fb
--- /dev/null
+++ b/internal/parse_test.go
@@ -0,0 +1,78 @@
+// Copyright (C) 2023 Luke Shumaker <lukeshu@lukeshu.com>
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package internal
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParserHandleRune(t *testing.T) {
+ t.Parallel()
+ type testcase struct {
+ Input string
+ ExpStack []string
+ }
+ testcases := map[string]testcase{
+ // Keep these test-cases in-sync with the examples in parse.go.
+ "object": {
+ Input: `{"x":"y","a":"b"}`,
+ ExpStack: []string{
+ // st,// processed
+ `?`,
+ `{`, // {
+ `»"`, // {"
+ `»"`, // {"x
+ `»`, // {"x"
+ `o?`, // {"x":
+ `o"`, // {"x":"
+ `o"`, // {"x":"y
+ `o`, // {"x":"y"
+ `{`, // {"x":"y",
+ `»"`, // {"x":"y","
+ `»"`, // {"x":"y","a
+ `»`, // {"x":"y","a"
+ `o?`, // {"x":"y","a":
+ `o"`, // {"x":"y","a":"
+ `o"`, // {"x":"y","a":"b
+ `o`, // {"x":"y","a":"b"
+ ``, // {"x":"y","a":"b"}
+ },
+ },
+ "array": {
+ Input: `["x","y"]`,
+ ExpStack: []string{
+ // st,// processed
+ `?`,
+ `[`, // [
+ `a"`, // ["
+ `a"`, // ["x
+ `a`, // ["x"
+ `a?`, // ["x",
+ `a"`, // ["x","
+ `a"`, // ["x","y
+ `a`, // ["x","y"
+ ``, // ["x","y"]
+ },
+ },
+ }
+ for tcName, tc := range testcases {
+ tc := tc
+ t.Run(tcName, func(t *testing.T) {
+ t.Parallel()
+ var par Parser
+ if !assert.Equal(t, len(tc.Input)+1, len(tc.ExpStack)) {
+ return
+ }
+ for i, r := range tc.Input {
+ assert.Equal(t, tc.ExpStack[i], par.stackString())
+ _, err := par.HandleRune(r)
+ assert.NoError(t, err)
+ assert.Equal(t, tc.ExpStack[i+1], par.stackString())
+ }
+ })
+ }
+}
diff --git a/internal/tags.go b/internal/tags.go
new file mode 100644
index 0000000..bdf1f72
--- /dev/null
+++ b/internal/tags.go
@@ -0,0 +1,7 @@
+// Copyright (C) 2022-2023 Luke Shumaker <lukeshu@lukeshu.com>
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package internal
+
+var ParseTag = parseTag
diff --git a/ioutil.go b/ioutil.go
new file mode 100644
index 0000000..a53eac3
--- /dev/null
+++ b/ioutil.go
@@ -0,0 +1,31 @@
+// Copyright (C) 2022-2023 Luke Shumaker <lukeshu@lukeshu.com>
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package lowmemjson
+
+import (
+ "io"
+ "unicode/utf8"
+)
+
+func writeByte(w io.Writer, c byte) error {
+ if br, ok := w.(interface{ WriteByte(byte) error }); ok {
+ return br.WriteByte(c)
+ }
+ var buf [1]byte
+ buf[0] = c
+ if _, err := w.Write(buf[:]); err != nil {
+ return err
+ }
+ return nil
+}
+
+func writeRune(w io.Writer, c rune) (int, error) {
+ if rw, ok := w.(interface{ WriteRune(rune) (int, error) }); ok {
+ return rw.WriteRune(c)
+ }
+ var buf [utf8.UTFMax]byte
+ n := utf8.EncodeRune(buf[:], c)
+ return w.Write(buf[:n])
+}
diff --git a/methods_test.go b/methods_test.go
index 5e2209a..f5d5a9a 100644
--- a/methods_test.go
+++ b/methods_test.go
@@ -6,8 +6,10 @@ package lowmemjson_test
import (
"bytes"
+ "errors"
"fmt"
"io"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -121,3 +123,199 @@ func TestMethods(t *testing.T) {
assert.NoError(t, lowmemjson.NewDecoder(&buf).Decode(&out))
assert.Equal(t, in, out)
}
+
+type strEncoder string
+
+func (s strEncoder) EncodeJSON(w io.Writer) error {
+ _, err := io.WriteString(w, string(s))
+ return err
+}
+
+type strMarshaler string
+
+func (s strMarshaler) MarshalJSON() ([]byte, error) {
+ return []byte(s), nil
+}
+
+type strTextMarshaler struct {
+ str string
+ err string
+}
+
+func (m strTextMarshaler) MarshalText() (txt []byte, err error) {
+ if len(m.str) > 0 {
+ txt = []byte(m.str)
+ }
+ if len(m.err) > 0 {
+ err = errors.New(m.err)
+ }
+ return
+}
+
+func TestMethodsEncode(t *testing.T) {
+ t.Parallel()
+ type testcase struct {
+ In string
+ ExpectedErr string
+ }
+ testcases := map[string]testcase{
+ "basic": {In: `{}`},
+ "empty": {In: ``, ExpectedErr: `syntax error at input byte 0: EOF`},
+ "short": {In: `{`, ExpectedErr: `syntax error at input byte 1: unexpected EOF`},
+ "long": {In: `{}{}`, ExpectedErr: `syntax error at input byte 2: invalid character '{' after top-level value`},
+ }
+ t.Run("encodable", func(t *testing.T) {
+ t.Parallel()
+ for tcName, tc := range testcases {
+ tc := tc
+ t.Run(tcName, func(t *testing.T) {
+ t.Parallel()
+ var buf strings.Builder
+ err := lowmemjson.NewEncoder(&buf).Encode([]any{strEncoder(tc.In)})
+ if tc.ExpectedErr == "" {
+ assert.NoError(t, err)
+ assert.Equal(t, "["+tc.In+"]", buf.String())
+ } else {
+ assert.EqualError(t, err,
+ `json: error calling EncodeJSON for type lowmemjson_test.strEncoder: `+
+ tc.ExpectedErr)
+ }
+ })
+ }
+ })
+ t.Run("marshaler", func(t *testing.T) {
+ t.Parallel()
+ for tcName, tc := range testcases {
+ tc := tc
+ t.Run(tcName, func(t *testing.T) {
+ t.Parallel()
+ var buf strings.Builder
+ err := lowmemjson.NewEncoder(&buf).Encode([]any{strMarshaler(tc.In)})
+ if tc.ExpectedErr == "" {
+ assert.NoError(t, err)
+ assert.Equal(t, "["+tc.In+"]", buf.String())
+ } else {
+ assert.EqualError(t, err,
+ `json: error calling MarshalJSON for type lowmemjson_test.strMarshaler: `+
+ tc.ExpectedErr)
+ }
+ })
+ }
+ })
+ t.Run("text", func(t *testing.T) {
+ t.Parallel()
+ type testcase struct {
+ Str string
+ Err string
+ }
+ testcases := map[string]testcase{
+ "basic": {Str: `a`},
+ "err": {Err: `xxx`},
+ "both": {Str: `a`, Err: `xxx`},
+ }
+ for tcName, tc := range testcases {
+ tc := tc
+ t.Run(tcName, func(t *testing.T) {
+ t.Parallel()
+ var buf strings.Builder
+ err := lowmemjson.NewEncoder(&buf).Encode([]any{strTextMarshaler{str: tc.Str, err: tc.Err}})
+ if tc.Err == "" {
+ assert.NoError(t, err)
+ assert.Equal(t, `["`+tc.Str+`"]`, buf.String())
+ } else {
+ assert.EqualError(t, err,
+ `json: error calling MarshalText for type lowmemjson_test.strTextMarshaler: `+
+ tc.Err)
+ assert.Equal(t, "[", buf.String())
+ }
+ })
+ }
+ })
+}
+
+type tstDecoder struct {
+ n int
+ err string
+}
+
+func (d *tstDecoder) DecodeJSON(r io.RuneScanner) error {
+ for i := 0; i < d.n; i++ {
+ if _, _, err := r.ReadRune(); err != nil {
+ if err == io.EOF {
+ break
+ }
+ return err
+ }
+ }
+ if len(d.err) > 0 {
+ return errors.New(d.err)
+ }
+ return nil
+}
+
+type strUnmarshaler struct {
+ err string
+}
+
+func (u *strUnmarshaler) UnmarshalJSON([]byte) error {
+ if u.err == "" {
+ return nil
+ }
+ return errors.New(u.err)
+}
+
+type textUnmarshaler struct {
+ err string
+}
+
+func (u *textUnmarshaler) UnmarshalText([]byte) error {
+ if u.err == "" {
+ return nil
+ }
+ return errors.New(u.err)
+}
+
+type errTextUnmarshaler struct {
+ S string
+}
+
+func (u *errTextUnmarshaler) UnmarshalText(dat []byte) error {
+ u.S = string(dat)
+ return errors.New("eee")
+}
+
+func TestMethodsDecode(t *testing.T) {
+ t.Parallel()
+ type testcase struct {
+ In string
+ Obj any
+ ExpectedErr string
+ }
+ testcases := map[string]testcase{
+ "decode-basic": {In: `{}`, Obj: &tstDecoder{n: 2}},
+ "decode-basic-eof": {In: `{}`, Obj: &tstDecoder{n: 5}},
+ "decode-syntax-error": {In: `{x}`, Obj: &tstDecoder{n: 5}, ExpectedErr: `json: v: syntax error at input byte 1: object: unexpected character: 'x'`},
+ "unmarshal-syntax-error": {In: `{x}`, Obj: &strUnmarshaler{}, ExpectedErr: `json: v: syntax error at input byte 1: object: unexpected character: 'x'`},
+ "decode-short": {In: `{}`, Obj: &tstDecoder{n: 1}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.tstDecoder: did not consume entire object`},
+ "decode-err": {In: `{}`, Obj: &tstDecoder{err: "xxx"}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.tstDecoder: xxx`},
+ "decode-err2": {In: `{}`, Obj: &tstDecoder{n: 1, err: "yyy"}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.tstDecoder: yyy`},
+ "unmarshal-err": {In: `{}`, Obj: &strUnmarshaler{err: "zzz"}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.strUnmarshaler: zzz`},
+ "unmarshaltext": {In: `""`, Obj: &textUnmarshaler{}},
+ "unmarshaltext-nonstr": {In: `{}`, Obj: &textUnmarshaler{}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.textUnmarshaler`},
+ "unmarshaltext-err": {In: `""`, Obj: &textUnmarshaler{err: "zzz"}, ExpectedErr: `json: v: cannot decode JSON string at input byte 0 into Go *lowmemjson_test.textUnmarshaler: zzz`},
+ "unmarshaltext-mapkey": {In: `{"a":1}`, Obj: new(map[errTextUnmarshaler]int), ExpectedErr: `json: v: cannot decode JSON string at input byte 1 into Go *lowmemjson_test.errTextUnmarshaler: eee`},
+ }
+ for tcName, tc := range testcases {
+ tc := tc
+ t.Run(tcName, func(t *testing.T) {
+ t.Parallel()
+ obj := tc.Obj
+ err := lowmemjson.NewDecoder(strings.NewReader(tc.In)).Decode(&obj)
+ if tc.ExpectedErr == "" {
+ assert.NoError(t, err)
+ } else {
+ assert.EqualError(t, err, tc.ExpectedErr)
+ }
+ })
+ }
+}
diff --git a/struct.go b/struct.go
index 24b2ac0..b7fc287 100644
--- a/struct.go
+++ b/struct.go
@@ -6,6 +6,8 @@ package lowmemjson
import (
"reflect"
+
+ "git.lukeshu.com/go/lowmemjson/internal"
)
type structField struct {
@@ -143,7 +145,7 @@ func indexStructInner(typ reflect.Type, byPos *[]structField, byName map[string]
if tag == "-" {
continue
}
- tagName, opts := parseTag(tag)
+ tagName, opts := internal.ParseTag(tag)
name := tagName
if !isValidTag(name) {
name = ""
diff --git a/test_export.go b/test_export.go
deleted file mode 100644
index 76d29d2..0000000
--- a/test_export.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (C) 2022 Luke Shumaker <lukeshu@lukeshu.com>
-//
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package lowmemjson
-
-import (
- "io"
-
- "git.lukeshu.com/go/lowmemjson/internal"
-)
-
-func init() {
- internal.EncodeStringFromString = func(w io.Writer, s string) { encodeStringFromString(w, nil, s) }
- internal.EncodeStringFromBytes = func(w io.Writer, s []byte) { encodeStringFromBytes(w, nil, s) }
-}
-
-var parseTag = internal.ParseTag