mirror of https://github.com/tidwall/tile38.git
620 lines
15 KiB
Go
620 lines
15 KiB
Go
|
package resp
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"bytes"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"strconv"
|
||
|
)
|
||
|
|
||
|
const bufsz = 4096
|
||
|
|
||
|
// Type represents a Value type
|
||
|
type Type byte
|
||
|
|
||
|
const (
|
||
|
SimpleString Type = '+'
|
||
|
Error Type = '-'
|
||
|
Integer Type = ':'
|
||
|
BulkString Type = '$'
|
||
|
Array Type = '*'
|
||
|
)
|
||
|
|
||
|
// TypeName returns name of the underlying RESP type.
|
||
|
func (t Type) String() string {
|
||
|
switch t {
|
||
|
default:
|
||
|
return "Unknown"
|
||
|
case '+':
|
||
|
return "SimpleString"
|
||
|
case '-':
|
||
|
return "Error"
|
||
|
case ':':
|
||
|
return "Integer"
|
||
|
case '$':
|
||
|
return "BulkString"
|
||
|
case '*':
|
||
|
return "Array"
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Value represents the data of a valid RESP type.
|
||
|
type Value struct {
|
||
|
typ Type
|
||
|
integer int
|
||
|
str []byte
|
||
|
array []Value
|
||
|
null bool
|
||
|
}
|
||
|
|
||
|
// Integer converts Value to an int. If Value cannot be converted, Zero is returned.
|
||
|
func (v Value) Integer() int {
|
||
|
switch v.typ {
|
||
|
default:
|
||
|
n, _ := strconv.ParseInt(v.String(), 10, 64)
|
||
|
return int(n)
|
||
|
case ':':
|
||
|
return v.integer
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// String converts Value to a string.
|
||
|
func (v Value) String() string {
|
||
|
if v.typ == '$' {
|
||
|
return string(v.str)
|
||
|
}
|
||
|
switch v.typ {
|
||
|
case '+', '-':
|
||
|
return string(v.str)
|
||
|
case ':':
|
||
|
return strconv.FormatInt(int64(v.integer), 10)
|
||
|
case '*':
|
||
|
return fmt.Sprintf("%v", v.array)
|
||
|
}
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
// Bytes converts the Value to a byte array. An empty string is converted to a non-nil empty byte array. If it's a RESP Null value, nil is returned.
|
||
|
func (v Value) Bytes() []byte {
|
||
|
switch v.typ {
|
||
|
default:
|
||
|
return []byte(v.String())
|
||
|
case '$', '+', '-':
|
||
|
return v.str
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Float converts Value to a float64. If Value cannot be converted, Zero is returned.
|
||
|
func (v Value) Float() float64 {
|
||
|
switch v.typ {
|
||
|
default:
|
||
|
f, _ := strconv.ParseFloat(v.String(), 64)
|
||
|
return f
|
||
|
case ':':
|
||
|
return float64(v.integer)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// IsNull indicates whether or not the base value is null.
|
||
|
func (v Value) IsNull() bool {
|
||
|
return v.null
|
||
|
}
|
||
|
|
||
|
// Bool converts Value to an bool. If Value cannot be converted, false is returned.
|
||
|
func (v Value) Bool() bool {
|
||
|
return v.Integer() != 0
|
||
|
}
|
||
|
|
||
|
// Error converts the Value to an error. If Value is not an error, nil is returned.
|
||
|
func (v Value) Error() error {
|
||
|
switch v.typ {
|
||
|
case '-':
|
||
|
return errors.New(string(v.str))
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Array converts the Value to a an array. If Value is not an array or when it's is a RESP Null value, nil is returned.
|
||
|
func (v Value) Array() []Value {
|
||
|
if v.typ == '*' && !v.null {
|
||
|
return v.array
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Type returns the underlying RESP type. The following types are represent valid RESP values.
|
||
|
// '+' SimpleString
|
||
|
// '-' Error
|
||
|
// ':' Integer
|
||
|
// '$' BulkString
|
||
|
// '*' Array
|
||
|
func (v Value) Type() Type {
|
||
|
return v.typ
|
||
|
}
|
||
|
|
||
|
func marshalSimpleRESP(typ Type, b []byte) ([]byte, error) {
|
||
|
bb := make([]byte, 3+len(b))
|
||
|
bb[0] = byte(typ)
|
||
|
copy(bb[1:], b)
|
||
|
bb[1+len(b)+0] = '\r'
|
||
|
bb[1+len(b)+1] = '\n'
|
||
|
return bb, nil
|
||
|
}
|
||
|
|
||
|
func marshalBulkRESP(v Value) ([]byte, error) {
|
||
|
if v.null {
|
||
|
return []byte("$-1\r\n"), nil
|
||
|
}
|
||
|
szb := []byte(strconv.FormatInt(int64(len(v.str)), 10))
|
||
|
bb := make([]byte, 5+len(szb)+len(v.str))
|
||
|
bb[0] = '$'
|
||
|
copy(bb[1:], szb)
|
||
|
bb[1+len(szb)+0] = '\r'
|
||
|
bb[1+len(szb)+1] = '\n'
|
||
|
copy(bb[1+len(szb)+2:], v.str)
|
||
|
bb[1+len(szb)+2+len(v.str)+0] = '\r'
|
||
|
bb[1+len(szb)+2+len(v.str)+1] = '\n'
|
||
|
return bb, nil
|
||
|
}
|
||
|
|
||
|
func marshalArrayRESP(v Value) ([]byte, error) {
|
||
|
if v.null {
|
||
|
return []byte("*-1\r\n"), nil
|
||
|
}
|
||
|
szb := []byte(strconv.FormatInt(int64(len(v.array)), 10))
|
||
|
|
||
|
var buf bytes.Buffer
|
||
|
buf.Grow(3 + len(szb) + 16*len(v.array)) // prime the buffer
|
||
|
buf.WriteByte('*')
|
||
|
buf.Write(szb)
|
||
|
buf.WriteByte('\r')
|
||
|
buf.WriteByte('\n')
|
||
|
for i := 0; i < len(v.array); i++ {
|
||
|
data, err := v.array[i].MarshalRESP()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
buf.Write(data)
|
||
|
}
|
||
|
return buf.Bytes(), nil
|
||
|
}
|
||
|
|
||
|
func marshalAnyRESP(v Value) ([]byte, error) {
|
||
|
switch v.typ {
|
||
|
default:
|
||
|
if v.typ == 0 && v.null {
|
||
|
return []byte("$-1\r\n"), nil
|
||
|
}
|
||
|
return nil, errors.New("unknown resp type encountered")
|
||
|
case '-', '+':
|
||
|
return marshalSimpleRESP(v.typ, v.str)
|
||
|
case ':':
|
||
|
return marshalSimpleRESP(v.typ, []byte(strconv.FormatInt(int64(v.integer), 10)))
|
||
|
case '$':
|
||
|
return marshalBulkRESP(v)
|
||
|
case '*':
|
||
|
return marshalArrayRESP(v)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Equals compares one value to another value.
|
||
|
func (v Value) Equals(value Value) bool {
|
||
|
data1, err := v.MarshalRESP()
|
||
|
if err != nil {
|
||
|
return false
|
||
|
}
|
||
|
data2, err := value.MarshalRESP()
|
||
|
if err != nil {
|
||
|
return false
|
||
|
}
|
||
|
return string(data1) == string(data2)
|
||
|
}
|
||
|
|
||
|
// MarshalRESP returns the original serialized byte representation of Value.
|
||
|
// For more information on this format please see http://redis.io/topics/protocol.
|
||
|
func (v Value) MarshalRESP() ([]byte, error) {
|
||
|
return marshalAnyRESP(v)
|
||
|
}
|
||
|
|
||
|
var nullValue = Value{null: true}
|
||
|
|
||
|
type errProtocol struct{ msg string }
|
||
|
|
||
|
func (err errProtocol) Error() string {
|
||
|
return "Protocol error: " + err.msg
|
||
|
}
|
||
|
|
||
|
// Reader is a specialized RESP Value type reader.
|
||
|
type Reader struct {
|
||
|
rd *bufio.Reader
|
||
|
}
|
||
|
|
||
|
// NewReader returns a Reader for reading Value types.
|
||
|
func NewReader(rd io.Reader) *Reader {
|
||
|
r := &Reader{rd: bufio.NewReader(rd)}
|
||
|
return r
|
||
|
}
|
||
|
|
||
|
// ReadValue reads the next Value from Reader.
|
||
|
func (rd *Reader) ReadValue() (value Value, n int, err error) {
|
||
|
value, _, n, err = rd.readValue(false, false)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// ReadMultiBulk reads the next multi bulk Value from Reader.
|
||
|
// A multi bulk value is a RESP array that contains one or more bulk strings.
|
||
|
// For more information on RESP arrays and strings please see http://redis.io/topics/protocol.
|
||
|
func (rd *Reader) ReadMultiBulk() (value Value, telnet bool, n int, err error) {
|
||
|
return rd.readValue(true, false)
|
||
|
}
|
||
|
|
||
|
func (rd *Reader) readValue(multibulk, child bool) (val Value, telnet bool, n int, err error) {
|
||
|
var rn int
|
||
|
var c byte
|
||
|
c, err = rd.rd.ReadByte()
|
||
|
if err != nil {
|
||
|
return nullValue, false, n, err
|
||
|
}
|
||
|
n++
|
||
|
if c == '*' {
|
||
|
val, rn, err = rd.readArrayValue(multibulk)
|
||
|
} else if multibulk && !child {
|
||
|
telnet = true
|
||
|
} else {
|
||
|
switch c {
|
||
|
default:
|
||
|
if multibulk && child {
|
||
|
return nullValue, telnet, n, &errProtocol{"expected '$', got '" + string(c) + "'"}
|
||
|
}
|
||
|
if child {
|
||
|
return nullValue, telnet, n, &errProtocol{"unknown first byte"}
|
||
|
}
|
||
|
telnet = true
|
||
|
case '-', '+':
|
||
|
val, rn, err = rd.readSimpleValue(c)
|
||
|
case ':':
|
||
|
val, rn, err = rd.readIntegerValue()
|
||
|
case '$':
|
||
|
val, rn, err = rd.readBulkValue()
|
||
|
}
|
||
|
}
|
||
|
if telnet {
|
||
|
n--
|
||
|
rd.rd.UnreadByte()
|
||
|
val, rn, err = rd.readTelnetMultiBulk()
|
||
|
if err == nil {
|
||
|
telnet = true
|
||
|
}
|
||
|
}
|
||
|
n += rn
|
||
|
if err == io.EOF {
|
||
|
return nullValue, telnet, n, io.ErrUnexpectedEOF
|
||
|
}
|
||
|
return val, telnet, n, err
|
||
|
}
|
||
|
|
||
|
func (rd *Reader) readTelnetMultiBulk() (v Value, n int, err error) {
|
||
|
values := make([]Value, 0, 8)
|
||
|
var c byte
|
||
|
var bline []byte
|
||
|
var quote, mustspace bool
|
||
|
for {
|
||
|
c, err = rd.rd.ReadByte()
|
||
|
if err != nil {
|
||
|
return nullValue, n, err
|
||
|
}
|
||
|
n += 1
|
||
|
if c == '\n' {
|
||
|
if len(bline) > 0 && bline[len(bline)-1] == '\r' {
|
||
|
bline = bline[:len(bline)-1]
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
if mustspace && c != ' ' {
|
||
|
return nullValue, n, &errProtocol{"unbalanced quotes in request"}
|
||
|
}
|
||
|
if c == ' ' {
|
||
|
if quote {
|
||
|
bline = append(bline, c)
|
||
|
} else {
|
||
|
values = append(values, Value{typ: '$', str: bline})
|
||
|
bline = nil
|
||
|
}
|
||
|
} else if c == '"' {
|
||
|
if quote {
|
||
|
mustspace = true
|
||
|
} else {
|
||
|
if len(bline) > 0 {
|
||
|
return nullValue, n, &errProtocol{"unbalanced quotes in request"}
|
||
|
}
|
||
|
quote = true
|
||
|
}
|
||
|
} else {
|
||
|
bline = append(bline, c)
|
||
|
}
|
||
|
}
|
||
|
if quote {
|
||
|
return nullValue, n, &errProtocol{"unbalanced quotes in request"}
|
||
|
}
|
||
|
if len(bline) > 0 {
|
||
|
values = append(values, Value{typ: '$', str: bline})
|
||
|
}
|
||
|
return Value{typ: '*', array: values}, n, nil
|
||
|
}
|
||
|
|
||
|
func (rd *Reader) readSimpleValue(typ byte) (val Value, n int, err error) {
|
||
|
var line []byte
|
||
|
line, n, err = rd.readLine()
|
||
|
if err != nil {
|
||
|
return nullValue, n, err
|
||
|
}
|
||
|
return Value{typ: Type(typ), str: line}, n, nil
|
||
|
}
|
||
|
func (rd *Reader) readLine() (line []byte, n int, err error) {
|
||
|
for {
|
||
|
b, err := rd.rd.ReadBytes('\n')
|
||
|
if err != nil {
|
||
|
return nil, 0, err
|
||
|
}
|
||
|
n += len(b)
|
||
|
line = append(line, b...)
|
||
|
if len(line) >= 2 && line[len(line)-2] == '\r' {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
return line[:len(line)-2], n, nil
|
||
|
}
|
||
|
func (rd *Reader) readBulkValue() (val Value, n int, err error) {
|
||
|
var rn int
|
||
|
var l int
|
||
|
l, rn, err = rd.readInt()
|
||
|
n += rn
|
||
|
if err != nil {
|
||
|
if _, ok := err.(*errProtocol); ok {
|
||
|
return nullValue, n, &errProtocol{"invalid bulk length"}
|
||
|
}
|
||
|
return nullValue, n, err
|
||
|
}
|
||
|
if l < 0 {
|
||
|
return Value{typ: '$', null: true}, n, nil
|
||
|
}
|
||
|
if l > 512*1024*1024 {
|
||
|
return nullValue, n, &errProtocol{"invalid bulk length"}
|
||
|
}
|
||
|
b := make([]byte, l+2)
|
||
|
rn, err = io.ReadFull(rd.rd, b)
|
||
|
n += rn
|
||
|
if err != nil {
|
||
|
return nullValue, n, err
|
||
|
}
|
||
|
if b[l] != '\r' || b[l+1] != '\n' {
|
||
|
return nullValue, n, &errProtocol{"invalid bulk line ending"}
|
||
|
}
|
||
|
return Value{typ: '$', str: b[:l]}, n, nil
|
||
|
}
|
||
|
|
||
|
func (rd *Reader) readArrayValue(multibulk bool) (val Value, n int, err error) {
|
||
|
var rn int
|
||
|
var l int
|
||
|
l, rn, err = rd.readInt()
|
||
|
n += rn
|
||
|
if err != nil || l > 1024*1024 {
|
||
|
if _, ok := err.(*errProtocol); ok {
|
||
|
if multibulk {
|
||
|
return nullValue, n, &errProtocol{"invalid multibulk length"}
|
||
|
}
|
||
|
return nullValue, n, &errProtocol{"invalid array length"}
|
||
|
}
|
||
|
return nullValue, n, err
|
||
|
}
|
||
|
if l < 0 {
|
||
|
return Value{typ: '*', null: true}, n, nil
|
||
|
}
|
||
|
var aval Value
|
||
|
vals := make([]Value, l)
|
||
|
for i := 0; i < l; i++ {
|
||
|
aval, _, rn, err = rd.readValue(multibulk, true)
|
||
|
n += rn
|
||
|
if err != nil {
|
||
|
return nullValue, n, err
|
||
|
}
|
||
|
vals[i] = aval
|
||
|
}
|
||
|
return Value{typ: '*', array: vals}, n, nil
|
||
|
}
|
||
|
|
||
|
func (rd *Reader) readIntegerValue() (val Value, n int, err error) {
|
||
|
var l int
|
||
|
l, n, err = rd.readInt()
|
||
|
if err != nil {
|
||
|
if _, ok := err.(*errProtocol); ok {
|
||
|
return nullValue, n, &errProtocol{"invalid integer"}
|
||
|
}
|
||
|
return nullValue, n, err
|
||
|
}
|
||
|
return Value{typ: ':', integer: l}, n, nil
|
||
|
}
|
||
|
|
||
|
func (rd *Reader) readInt() (x int, n int, err error) {
|
||
|
line, n, err := rd.readLine()
|
||
|
if err != nil {
|
||
|
return 0, 0, err
|
||
|
}
|
||
|
i64, err := strconv.ParseInt(string(line), 10, 64)
|
||
|
if err != nil {
|
||
|
return 0, n, err
|
||
|
}
|
||
|
return int(i64), n, nil
|
||
|
}
|
||
|
|
||
|
// AnyValue returns a RESP value from an interface. This function infers the types. Arrays are not allowed.
|
||
|
func AnyValue(v interface{}) Value {
|
||
|
switch v := v.(type) {
|
||
|
default:
|
||
|
return StringValue(fmt.Sprintf("%v", v))
|
||
|
case nil:
|
||
|
return NullValue()
|
||
|
case int:
|
||
|
return IntegerValue(int(v))
|
||
|
case uint:
|
||
|
return IntegerValue(int(v))
|
||
|
case int8:
|
||
|
return IntegerValue(int(v))
|
||
|
case uint8:
|
||
|
return IntegerValue(int(v))
|
||
|
case int16:
|
||
|
return IntegerValue(int(v))
|
||
|
case uint16:
|
||
|
return IntegerValue(int(v))
|
||
|
case int32:
|
||
|
return IntegerValue(int(v))
|
||
|
case uint32:
|
||
|
return IntegerValue(int(v))
|
||
|
case int64:
|
||
|
return IntegerValue(int(v))
|
||
|
case uint64:
|
||
|
return IntegerValue(int(v))
|
||
|
case bool:
|
||
|
return BoolValue(v)
|
||
|
case float32:
|
||
|
return FloatValue(float64(v))
|
||
|
case float64:
|
||
|
return FloatValue(float64(v))
|
||
|
case []byte:
|
||
|
return BytesValue(v)
|
||
|
case string:
|
||
|
return StringValue(v)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// SimpleStringValue returns a RESP simple string. A simple string has no new lines. The carriage return and new line characters are replaced with spaces.
|
||
|
func SimpleStringValue(s string) Value { return Value{typ: '+', str: []byte(formSingleLine(s))} }
|
||
|
|
||
|
// BytesValue returns a RESP bulk string. A bulk string can represent any data.
|
||
|
func BytesValue(b []byte) Value { return Value{typ: '$', str: b} }
|
||
|
|
||
|
// StringValue returns a RESP bulk string. A bulk string can represent any data.
|
||
|
func StringValue(s string) Value { return Value{typ: '$', str: []byte(s)} }
|
||
|
|
||
|
// NullValue returns a RESP null bulk string.
|
||
|
func NullValue() Value { return Value{typ: '$', null: true} }
|
||
|
|
||
|
// ErrorValue returns a RESP error.
|
||
|
func ErrorValue(err error) Value {
|
||
|
if err == nil {
|
||
|
return Value{typ: '-'}
|
||
|
}
|
||
|
return Value{typ: '-', str: []byte(err.Error())}
|
||
|
}
|
||
|
|
||
|
// IntegerValue returns a RESP integer.
|
||
|
func IntegerValue(i int) Value { return Value{typ: ':', integer: i} }
|
||
|
|
||
|
// BoolValue returns a RESP integer representation of a bool.
|
||
|
func BoolValue(t bool) Value {
|
||
|
if t {
|
||
|
return Value{typ: ':', integer: 1}
|
||
|
}
|
||
|
return Value{typ: ':', integer: 0}
|
||
|
}
|
||
|
|
||
|
// FloatValue returns a RESP bulk string representation of a float.
|
||
|
func FloatValue(f float64) Value { return StringValue(strconv.FormatFloat(f, 'f', -1, 64)) }
|
||
|
|
||
|
// ArrayValue returns a RESP array.
|
||
|
func ArrayValue(vals []Value) Value { return Value{typ: '*', array: vals} }
|
||
|
|
||
|
func formSingleLine(s string) string {
|
||
|
bs1 := []byte(s)
|
||
|
for i := 0; i < len(bs1); i++ {
|
||
|
switch bs1[i] {
|
||
|
case '\r', '\n':
|
||
|
bs2 := make([]byte, len(bs1))
|
||
|
copy(bs2, bs1)
|
||
|
bs2[i] = ' '
|
||
|
i++
|
||
|
for ; i < len(bs2); i++ {
|
||
|
switch bs1[i] {
|
||
|
case '\r', '\n':
|
||
|
bs2[i] = ' '
|
||
|
}
|
||
|
}
|
||
|
return string(bs2)
|
||
|
}
|
||
|
}
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
// MultiBulkValue returns a RESP array which contains one or more bulk strings.
|
||
|
// For more information on RESP arrays and strings please see http://redis.io/topics/protocol.
|
||
|
func MultiBulkValue(commandName string, args ...interface{}) Value {
|
||
|
vals := make([]Value, len(args)+1)
|
||
|
vals[0] = StringValue(commandName)
|
||
|
for i, arg := range args {
|
||
|
if rval, ok := arg.(Value); ok && rval.Type() == BulkString {
|
||
|
vals[i+1] = rval
|
||
|
continue
|
||
|
}
|
||
|
switch arg := arg.(type) {
|
||
|
default:
|
||
|
vals[i+1] = StringValue(fmt.Sprintf("%v", arg))
|
||
|
case []byte:
|
||
|
vals[i+1] = StringValue(string(arg))
|
||
|
case string:
|
||
|
vals[i+1] = StringValue(arg)
|
||
|
case nil:
|
||
|
vals[i+1] = NullValue()
|
||
|
}
|
||
|
}
|
||
|
return ArrayValue(vals)
|
||
|
}
|
||
|
|
||
|
// Writer is a specialized RESP Value type writer.
|
||
|
type Writer struct {
|
||
|
wr io.Writer
|
||
|
}
|
||
|
|
||
|
// NewWriter returns a new Writer.
|
||
|
func NewWriter(wr io.Writer) *Writer {
|
||
|
return &Writer{wr}
|
||
|
}
|
||
|
|
||
|
// WriteValue writes a RESP Value.
|
||
|
func (wr *Writer) WriteValue(v Value) error {
|
||
|
b, err := v.MarshalRESP()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
_, err = wr.wr.Write(b)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// WriteSimpleString writes a RESP simple string. A simple string has no new lines. The carriage return and new line characters are replaced with spaces.
|
||
|
func (wr *Writer) WriteSimpleString(s string) error { return wr.WriteValue(SimpleStringValue(s)) }
|
||
|
|
||
|
// WriteBytes writes a RESP bulk string. A bulk string can represent any data.
|
||
|
func (wr *Writer) WriteBytes(b []byte) error { return wr.WriteValue(BytesValue(b)) }
|
||
|
|
||
|
// WriteString writes a RESP bulk string. A bulk string can represent any data.
|
||
|
func (wr *Writer) WriteString(s string) error { return wr.WriteValue(StringValue(s)) }
|
||
|
|
||
|
// WriteNull writes a RESP null bulk string.
|
||
|
func (wr *Writer) WriteNull() error { return wr.WriteValue(NullValue()) }
|
||
|
|
||
|
// WriteError writes a RESP error.
|
||
|
func (wr *Writer) WriteError(err error) error { return wr.WriteValue(ErrorValue(err)) }
|
||
|
|
||
|
// WriteInteger writes a RESP integer.
|
||
|
func (wr *Writer) WriteInteger(i int) error { return wr.WriteValue(IntegerValue(i)) }
|
||
|
|
||
|
// WriteArray writes a RESP array.
|
||
|
func (wr *Writer) WriteArray(vals []Value) error { return wr.WriteValue(ArrayValue(vals)) }
|
||
|
|
||
|
// WriteMultiBulk writes a RESP array which contains one or more bulk strings.
|
||
|
// For more information on RESP arrays and strings please see http://redis.io/topics/protocol.
|
||
|
func (wr *Writer) WriteMultiBulk(commandName string, args ...interface{}) error {
|
||
|
return wr.WriteValue(MultiBulkValue(commandName, args...))
|
||
|
}
|