github.com/yandex/pandora@v0.5.32/components/providers/http/decoders/jsonline.go (about)

     1  package decoders
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"sync"
    12  
    13  	"github.com/yandex/pandora/components/providers/http/config"
    14  	"github.com/yandex/pandora/components/providers/http/decoders/ammo"
    15  	"github.com/yandex/pandora/core"
    16  	"golang.org/x/xerrors"
    17  )
    18  
    19  func newJsonlineDecoder(file io.ReadSeeker, cfg config.Config, decodedConfigHeaders http.Header) (*jsonlineDecoder, error) {
    20  	scanner := bufio.NewScanner(file)
    21  	if cfg.MaxAmmoSize != 0 {
    22  		var buffer []byte
    23  		scanner.Buffer(buffer, cfg.MaxAmmoSize)
    24  	}
    25  
    26  	isArray, err := isArray(file)
    27  	if err != nil {
    28  		return nil, err
    29  	}
    30  
    31  	decoder := &jsonlineDecoder{
    32  		protoDecoder: protoDecoder{
    33  			file:                 file,
    34  			config:               cfg,
    35  			decodedConfigHeaders: decodedConfigHeaders,
    36  		},
    37  		scanner: scanner,
    38  		pool:    &sync.Pool{New: func() any { return &ammo.Ammo{} }},
    39  		decoder: json.NewDecoder(file),
    40  	}
    41  	if isArray {
    42  		ammos, err := decoder.readArray()
    43  		if err != nil {
    44  			return decoder, fmt.Errorf("cant read json array: %w", err)
    45  		}
    46  		decoder.ammos = ammos
    47  	}
    48  	return decoder, nil
    49  }
    50  
    51  func isArray(r io.ReadSeeker) (bool, error) {
    52  	d := json.NewDecoder(r)
    53  	t, err := d.Token()
    54  	if err != nil {
    55  		return false, err
    56  	}
    57  	delim, ok := t.(json.Delim)
    58  	if !ok {
    59  		return false, errors.New("invalid json token")
    60  	}
    61  	_, err = r.Seek(0, io.SeekStart)
    62  	return delim.String() == "[", err
    63  }
    64  
    65  type jsonlineDecoder struct {
    66  	protoDecoder
    67  	scanner *bufio.Scanner
    68  	line    uint
    69  	pool    *sync.Pool
    70  	decoder *json.Decoder
    71  	ammos   []DecodedAmmo
    72  }
    73  
    74  func (d *jsonlineDecoder) Release(a core.Ammo) {
    75  	if am, ok := a.(*ammo.Ammo); ok {
    76  		am.Reset()
    77  		d.pool.Put(am)
    78  	}
    79  }
    80  
    81  func (d *jsonlineDecoder) LoadAmmo(ctx context.Context) ([]DecodedAmmo, error) {
    82  	return d.protoDecoder.LoadAmmo(ctx, d.Scan)
    83  }
    84  
    85  type entity struct {
    86  	// Host defines Host header to send.
    87  	// Request endpoint is defied by gun config.
    88  	Host   string `json:"host"`
    89  	Method string `json:"method"`
    90  	URI    string `json:"uri"`
    91  	// Headers defines headers to send.
    92  	// NOTE: Host header will be silently ignored.
    93  	Headers map[string]string `json:"headers"`
    94  	Tag     string            `json:"tag"`
    95  	// Body should be string, doublequotes should be escaped for json body
    96  	Body string `json:"body"`
    97  }
    98  
    99  func (d *jsonlineDecoder) Scan(ctx context.Context) (DecodedAmmo, error) {
   100  	if d.config.Limit != 0 && d.ammoNum >= d.config.Limit {
   101  		return nil, ErrAmmoLimit
   102  	}
   103  	if d.ammos != nil {
   104  		return d.scanAmmos()
   105  	}
   106  	for {
   107  		if d.config.Passes != 0 && d.passNum >= d.config.Passes {
   108  			return nil, ErrPassLimit
   109  		}
   110  		var da entity
   111  		err := d.decoder.Decode(&da)
   112  		if err != nil {
   113  			if err != io.EOF {
   114  				return nil, xerrors.Errorf("failed to decode ammo at line: %v; with err: %w", d.line+1, err)
   115  			}
   116  			// go to next pass
   117  		} else {
   118  			d.line++
   119  			d.ammoNum++
   120  
   121  			header := d.decodedConfigHeaders.Clone()
   122  			for k, v := range da.Headers {
   123  				header.Set(k, v)
   124  			}
   125  			url := "http://" + da.Host + da.URI // schema will be rewrite in gun
   126  			var body []byte
   127  			if da.Body != "" {
   128  				body = []byte(da.Body)
   129  			}
   130  			a := d.pool.Get().(*ammo.Ammo)
   131  			err = a.Setup(da.Method, url, body, header, da.Tag)
   132  			return a, err
   133  		}
   134  
   135  		err = d.scanner.Err()
   136  		if err != nil {
   137  			return nil, err
   138  		}
   139  		if d.ammoNum == 0 {
   140  			return nil, ErrNoAmmo
   141  		}
   142  		d.line = 0
   143  		d.passNum++
   144  
   145  		_, err = d.file.Seek(0, io.SeekStart)
   146  		if err != nil {
   147  			return nil, err
   148  		}
   149  		d.decoder = json.NewDecoder(d.file)
   150  	}
   151  }
   152  
   153  func (d *jsonlineDecoder) readArray() ([]DecodedAmmo, error) {
   154  	var data []entity
   155  	err := d.decoder.Decode(&data)
   156  	if err != nil {
   157  		return nil, fmt.Errorf("cant readArray, err: %w", err)
   158  	}
   159  	result := make([]DecodedAmmo, len(data))
   160  	for i, datum := range data {
   161  		header := d.decodedConfigHeaders.Clone()
   162  		for k, v := range datum.Headers {
   163  			header.Set(k, v)
   164  		}
   165  		url := "http://" + datum.Host + datum.URI // schema will be rewrite in gun
   166  		var body []byte
   167  		if datum.Body != "" {
   168  			body = []byte(datum.Body)
   169  		}
   170  		a := d.pool.Get().(*ammo.Ammo)
   171  		err = a.Setup(datum.Method, url, body, header, datum.Tag)
   172  		if err != nil {
   173  			return nil, fmt.Errorf("cant readArray, err: %w", err)
   174  		}
   175  		result[i] = a
   176  	}
   177  
   178  	return result, nil
   179  }
   180  
   181  func (d *jsonlineDecoder) scanAmmos() (DecodedAmmo, error) {
   182  	length := len(d.ammos)
   183  	if length == 0 {
   184  		return nil, ErrNoAmmo
   185  	}
   186  	if d.config.Passes != 0 && d.passNum >= d.config.Passes {
   187  		return nil, ErrPassLimit
   188  	}
   189  	i := int(d.ammoNum) % length
   190  	a := d.ammos[i]
   191  	if d.ammoNum > 0 && i == length-1 {
   192  		d.passNum++
   193  	}
   194  	d.ammoNum++
   195  	return a, nil
   196  }