Files
search-go/encoding.go
Matt Van HornandJavad Rajabzadeh 0278e6e135 refactor: merge executeNDJSONRequest into executeRequest and validate accepted content-type
Per @ElyarSadig's review:

- Remove executeNDJSONRequest. NDJSON streaming is now handled inside
  executeRequest when req.acceptedContentType is set to NDJSON and
  req.withResponse is non-nil.
- Add acceptedContentType field on internalRequest (peer to acceptedStatusCodes)
  and validate the response Content-Type header against it. Mismatch
  returns a typed error.
- Add Decoder(io.Reader) (streamDecoder, error) method on the encoder
  interface so callers can stream-decode without bypassing the encoder
  abstraction.
- GetTaskDocumentsWithContext is now a thin wrapper that delegates to
  executeRequest with acceptedContentType set.
2026-05-29 17:40:35 +00:00

227 lines
4.7 KiB
Go

package meilisearch
import (
"bytes"
"compress/gzip"
"compress/zlib"
"encoding/json"
"github.com/andybalholm/brotli"
"io"
"sync"
)
type encoder interface {
Encode(io.Reader) (io.ReadCloser, error)
Decode([]byte, interface{}) error
Decoder(io.Reader) (streamDecoder, error)
}
type streamDecoder interface {
Decode(interface{}) error
Close() error
}
type jsonStreamDecoder struct {
*json.Decoder
closer io.Closer
}
func (d *jsonStreamDecoder) Close() error {
if d.closer == nil {
return nil
}
return d.closer.Close()
}
func newEncoding(ce ContentEncoding, level EncodingCompressionLevel) encoder {
switch ce {
case GzipEncoding:
return &gzipEncoder{
gzWriterPool: &sync.Pool{
New: func() interface{} {
w, err := gzip.NewWriterLevel(io.Discard, level.Int())
return &gzipWriter{writer: w, err: err}
},
},
bufferPool: &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }},
}
case DeflateEncoding:
return &flateEncoder{
flWriterPool: &sync.Pool{
New: func() interface{} {
w, err := zlib.NewWriterLevel(io.Discard, level.Int())
return &flateWriter{writer: w, err: err}
},
},
bufferPool: &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }},
}
case BrotliEncoding:
return &brotliEncoder{
brWriterPool: &sync.Pool{
New: func() interface{} {
return brotli.NewWriterLevel(io.Discard, level.Int())
},
},
bufferPool: &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }},
}
default:
return nil
}
}
type gzipEncoder struct {
gzWriterPool *sync.Pool
bufferPool *sync.Pool
}
type gzipWriter struct {
writer *gzip.Writer
err error
}
func (g *gzipEncoder) Encode(rc io.Reader) (io.ReadCloser, error) {
w := g.gzWriterPool.Get().(*gzipWriter)
defer g.gzWriterPool.Put(w)
if w.err != nil {
return nil, w.err
}
defer func() {
_ = w.writer.Close()
}()
buf := g.bufferPool.Get().(*bytes.Buffer)
buf.Reset()
w.writer.Reset(buf)
if _, err := copyZeroAlloc(w.writer, rc); err != nil {
g.bufferPool.Put(buf)
return nil, err
}
return &pooledBuffer{Buffer: buf, pool: g.bufferPool}, nil
}
func (g *gzipEncoder) Decode(data []byte, vPtr interface{}) error {
r, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return err
}
defer func() {
_ = r.Close()
}()
return json.NewDecoder(r).Decode(vPtr)
}
func (g *gzipEncoder) Decoder(r io.Reader) (streamDecoder, error) {
gr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
return &jsonStreamDecoder{Decoder: json.NewDecoder(gr), closer: gr}, nil
}
type flateEncoder struct {
flWriterPool *sync.Pool
bufferPool *sync.Pool
}
type flateWriter struct {
writer *zlib.Writer
err error
}
func (f *flateEncoder) Encode(rc io.Reader) (io.ReadCloser, error) {
w := f.flWriterPool.Get().(*flateWriter)
defer f.flWriterPool.Put(w)
if w.err != nil {
return nil, w.err
}
defer func() {
_ = w.writer.Close()
}()
buf := f.bufferPool.Get().(*bytes.Buffer)
buf.Reset()
w.writer.Reset(buf)
if _, err := copyZeroAlloc(w.writer, rc); err != nil {
f.bufferPool.Put(buf)
return nil, err
}
return &pooledBuffer{Buffer: buf, pool: f.bufferPool}, nil
}
func (f *flateEncoder) Decode(data []byte, vPtr interface{}) error {
r, err := zlib.NewReader(bytes.NewBuffer(data))
if err != nil {
return err
}
defer func() {
_ = r.Close()
}()
return json.NewDecoder(r).Decode(vPtr)
}
func (f *flateEncoder) Decoder(r io.Reader) (streamDecoder, error) {
zr, err := zlib.NewReader(r)
if err != nil {
return nil, err
}
return &jsonStreamDecoder{Decoder: json.NewDecoder(zr), closer: zr}, nil
}
type brotliEncoder struct {
brWriterPool *sync.Pool
bufferPool *sync.Pool
}
func (b *brotliEncoder) Encode(rc io.Reader) (io.ReadCloser, error) {
w := b.brWriterPool.Get().(*brotli.Writer)
defer func() {
_ = w.Close()
}()
defer b.brWriterPool.Put(w)
buf := b.bufferPool.Get().(*bytes.Buffer)
buf.Reset()
w.Reset(buf)
if _, err := copyZeroAlloc(w, rc); err != nil {
b.bufferPool.Put(buf)
return nil, err
}
return &pooledBuffer{Buffer: buf, pool: b.bufferPool}, nil
}
func (b *brotliEncoder) Decode(data []byte, vPtr interface{}) error {
r := brotli.NewReader(bytes.NewBuffer(data))
return json.NewDecoder(r).Decode(vPtr)
}
func (b *brotliEncoder) Decoder(r io.Reader) (streamDecoder, error) {
return &jsonStreamDecoder{Decoder: json.NewDecoder(brotli.NewReader(r))}, nil
}
var copyBufPool = sync.Pool{
New: func() interface{} {
return make([]byte, 4096)
},
}
func copyZeroAlloc(w io.Writer, r io.Reader) (int64, error) {
if wt, ok := r.(io.WriterTo); ok {
return wt.WriteTo(w)
}
if rt, ok := w.(io.ReaderFrom); ok {
return rt.ReadFrom(r)
}
vbuf := copyBufPool.Get()
buf := vbuf.([]byte)
n, err := io.CopyBuffer(w, r, buf)
copyBufPool.Put(vbuf)
return n, err
}