Skip to content

Commit

Permalink
Buffer writes in the Encoder into byte array
Browse files Browse the repository at this point in the history
In case the client did not use a buffered writer.
The client must now flush the Encoder explicitly.
  • Loading branch information
kaiburjack committed Feb 3, 2022
1 parent b90e642 commit 65cf8b1
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 48 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func TestDecodeAndEncode(t *testing.T) {
err = enc.EncodeToken(&tk)
assert.Nil(t, err)
}
assert.Nil(t, enc.Flush())

// then
assert.Equal(t,
Expand Down
3 changes: 3 additions & 0 deletions combined_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func BenchmarkNamespaceAlias1Level(b *testing.B) {
err = enc.EncodeToken(&tk)
assert.Nil(b, err)
}
assert.Nil(b, enc.Flush())
}
}

Expand Down Expand Up @@ -104,6 +105,7 @@ func BenchmarkSameNamespaceSideBySide(b *testing.B) {
err = enc.EncodeToken(&tk)
assert.Nil(b, err)
}
assert.Nil(b, enc.Flush())
}
}

Expand Down Expand Up @@ -414,4 +416,5 @@ func decodeEncode(t *testing.T, dec gosaxml.Decoder, enc *gosaxml.Encoder, tk *g
err = enc.EncodeToken(tk)
assert.Nil(t, err)
}
assert.Nil(t, enc.Flush())
}
116 changes: 68 additions & 48 deletions encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,10 @@ const (

// pre-allocate all constant byte slices that we write
var (
angleOpen = bs("<")
angleClose = bs(">")
slashAngleClose = bs("/>")
angleOpenSlash = bs("</")
space = bs(" ")
equal = bs("=")
angleOpenQuest = bs("<?")
questAngleClose = bs("?>")
colon = bs(":")
singleQuote = bs("'")
doubleQuote = bs("\"")
)

// EncoderMiddleware allows to pre-process a Token before
Expand All @@ -50,11 +43,17 @@ type EncoderMiddleware interface {

// Encoder encodes Token values to an io.Writer.
type Encoder struct {
// buffers writes to the underlying io.Writer
buf [2048]byte

// middlewares can modify encoded tokens before encoding.
middlewares []EncoderMiddleware

// The io.Writer we encode/write into.
w io.Writer
wr io.Writer

// the current write position into buf
w int

// Whether the last token was of type TokenTypeStartElement.
// This is used to delay encoding the ending ">" or "/>" string
Expand All @@ -65,15 +64,50 @@ type Encoder struct {
// NewEncoder creates a new Encoder with the given middlewares and returns a pointer to it.
func NewEncoder(w io.Writer, middlewares ...EncoderMiddleware) *Encoder {
return &Encoder{
w: w,
wr: w,
middlewares: middlewares,
}
}

// Flush writes all buffered output into the io.Writer.
// It must be called after token encoding is done in order
// to write all remaining bytes into the io.Writer.
func (thiz *Encoder) Flush() error {
_, err := thiz.wr.Write(thiz.buf[:thiz.w])
thiz.w = 0
return err
}

func (thiz *Encoder) write(b byte) error {
if thiz.w >= len(thiz.buf) {
err := thiz.Flush()
if err != nil {
return err
}
}
thiz.buf[thiz.w] = b
thiz.w++
return nil
}

func (thiz *Encoder) writeBytes(bs []byte) error {
l := len(bs)
if thiz.w+l > len(thiz.buf) {
err := thiz.Flush()
if err != nil {
return err
}
}
copy(thiz.buf[thiz.w:], bs)
thiz.w += l
return nil
}

// Reset resets this Encoder to write into the provided io.Writer
// and resets all middlewares.
func (thiz *Encoder) Reset(w io.Writer) {
thiz.w = w
thiz.wr = w
thiz.w = 0
thiz.lastStartElement = false
for _, middleware := range thiz.middlewares {
middleware.Reset()
Expand Down Expand Up @@ -127,7 +161,7 @@ func (thiz *Encoder) encodeStartElement(t *Token) error {
if err != nil {
return err
}
_, err = thiz.w.Write(angleOpen)
err = thiz.write('<')
if err != nil {
return err
}
Expand All @@ -146,15 +180,15 @@ func (thiz *Encoder) encodeStartElement(t *Token) error {
// write attributes
for i := 0; i < len(t.Attr); i++ {
attr := &t.Attr[i]
_, err = thiz.w.Write(space)
err = thiz.write(' ')
if err != nil {
return err
}
err = thiz.writeName(attr.Name)
if err != nil {
return err
}
_, err = thiz.w.Write(equal)
err = thiz.write('=')
if err != nil {
return err
}
Expand All @@ -174,7 +208,7 @@ func (thiz *Encoder) encodeEndElement(t *Token) error {
if thiz.lastStartElement {
// the last seen token was a StartElement, so this
// token can only be its accompanying EndElement.
_, err := thiz.w.Write(slashAngleClose)
err := thiz.writeBytes(slashAngleClose)
if err != nil {
return err
}
Expand All @@ -185,15 +219,15 @@ func (thiz *Encoder) encodeEndElement(t *Token) error {
if err != nil {
return err
}
_, err = thiz.w.Write(angleOpenSlash)
err = thiz.writeBytes(angleOpenSlash)
if err != nil {
return err
}
err = thiz.writeName(t.Name)
if err != nil {
return err
}
_, err = thiz.w.Write(angleClose)
err = thiz.write('>')
if err != nil {
return err
}
Expand All @@ -203,51 +237,47 @@ func (thiz *Encoder) encodeEndElement(t *Token) error {
func (thiz *Encoder) callMiddlewares(t *Token) error {
var err error
for _, middleware := range thiz.middlewares {
err = middleware.EncodeToken((*Token)(noescape(unsafe.Pointer(t))))
err = middleware.EncodeToken(t)
if err != nil {
return err
}
}
return nil
}

func (thiz Encoder) writeName(n Name) error {
func (thiz *Encoder) writeName(n Name) error {
var err error
if n.Prefix != nil {
_, err = thiz.w.Write(n.Prefix)
err = thiz.writeBytes(n.Prefix)
if err != nil {
return err
}
_, err = thiz.w.Write(colon)
err = thiz.write(':')
if err != nil {
return err
}
}
_, err = thiz.w.Write(n.Local)
if err != nil {
return err
}
return nil
return thiz.writeBytes(n.Local)
}

func (thiz Encoder) writeString(s []byte, useSingleQuote bool) error {
func (thiz *Encoder) writeString(s []byte, useSingleQuote bool) error {
var err error
if useSingleQuote {
_, err = thiz.w.Write(singleQuote)
err = thiz.write('\'')
} else {
_, err = thiz.w.Write(doubleQuote)
err = thiz.write('"')
}
if err != nil {
return err
}
_, err = thiz.w.Write(s)
err = thiz.writeBytes(s)
if err != nil {
return err
}
if useSingleQuote {
_, err = thiz.w.Write(singleQuote)
err = thiz.write('\'')
} else {
_, err = thiz.w.Write(doubleQuote)
err = thiz.write('"')
}
return err
}
Expand All @@ -257,14 +287,13 @@ func (thiz *Encoder) encodeTextElement(t *Token) error {
if err != nil {
return err
}
_, err = thiz.w.Write(t.ByteData)
return err
return thiz.writeBytes(t.ByteData)
}

func (thiz *Encoder) endLastStartElement() error {
if thiz.lastStartElement {
// end the last StartElement with its ">"
_, err := thiz.w.Write(angleClose)
err := thiz.write('>')
if err != nil {
return err
}
Expand All @@ -277,32 +306,31 @@ func (thiz *Encoder) encodeDirective(t *Token) error {
if err != nil {
return err
}
_, err = thiz.w.Write(t.ByteData)
return err
return thiz.writeBytes(t.ByteData)
}

func (thiz *Encoder) encodeProcInst(t *Token) error {
err := thiz.endLastStartElement()
if err != nil {
return err
}
_, err = thiz.w.Write(angleOpenQuest)
err = thiz.writeBytes(angleOpenQuest)
if err != nil {
return err
}
err = thiz.writeName(t.Name)
if err != nil {
return err
}
_, err = thiz.w.Write(space)
err = thiz.write(' ')
if err != nil {
return err
}
_, err = thiz.w.Write(t.ByteData)
err = thiz.writeBytes(t.ByteData)
if err != nil {
return err
}
_, err = thiz.w.Write(questAngleClose)
err = thiz.writeBytes(questAngleClose)
return err
}

Expand All @@ -312,11 +340,3 @@ func bs(s string) []byte {
(*reflect.StringHeader)(unsafe.Pointer(&s)).Data),
)[:len(s):len(s)]
}

// https://go.googlesource.com/go/+/go1.17.6/src/runtime/stubs.go#164
//go:nosplit
func noescape(p unsafe.Pointer) unsafe.Pointer {
x := uintptr(p)
//goland:noinspection ALL
return unsafe.Pointer(x)
}
4 changes: 4 additions & 0 deletions encoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func TestEncodeStartElement(t *testing.T) {
Value: []byte("https://mynamespace"),
}},
})
assert.Nil(t, enc.Flush())

// then
assert.Nil(t, err)
Expand Down Expand Up @@ -157,6 +158,7 @@ func TestEncodeStartElementEndElement(t *testing.T) {
Value: []byte("https://mynamespace"),
}},
})
assert.Nil(t, enc.Flush())

// then
assert.Nil(t, err1)
Expand Down Expand Up @@ -199,6 +201,7 @@ func TestEncodeTwoNestedWithRedundantNamespace(t *testing.T) {
Value: []byte("https://mynamespace"),
}},
})
assert.Nil(t, enc.Flush())

// then
assert.Nil(t, err1)
Expand Down Expand Up @@ -236,6 +239,7 @@ func TestEncodeTwoNestedWithRedundantNamespaceUnprefixed(t *testing.T) {
Value: []byte("https://mynamespace"),
}},
})
assert.Nil(t, enc.Flush())

// then
assert.Nil(t, err1)
Expand Down
1 change: 1 addition & 0 deletions usecases_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ soap:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
addEndElement(t, enc)
}
}
assert.Nil(t, enc.Flush())

// then
assert.Equal(t, "<a:Envelope xmlns:a=\"http://www.w3.org/2003/05/soap-envelope/\" "+
Expand Down

0 comments on commit 65cf8b1

Please sign in to comment.