Last active
October 3, 2018 17:00
-
-
Save ORBAT/4f31c831ff76358ffcad105870e77749 to your computer and use it in GitHub Desktop.
Message encoding example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"encoding/base64" | |
"encoding/json" | |
"errors" | |
"fmt" | |
"gopkg.in/vmihailenco/msgpack.v4" | |
"reflect" | |
"strings" | |
) | |
const ( | |
EncodingUTF8 = "utf-8" | |
EncodingJSON = "json" | |
EncodingBase64 = "base64" | |
EncodingXOR = "cipher+xor" | |
) | |
type Cipher interface { | |
Encrypt(data []byte) []byte | |
Decrypt(data []byte) []byte | |
} | |
type XORCipher byte | |
func (xc XORCipher) Encrypt(plaintext []byte) (ciphertext []byte) { | |
ciphertext = make([]byte, len(plaintext)) | |
for i, b := range plaintext { | |
ciphertext[i] = b ^ byte(xc) | |
} | |
return ciphertext | |
} | |
func (xc XORCipher) Decrypt(ciphertext []byte) (plaintext []byte) { | |
plaintext = make([]byte, len(ciphertext)) | |
for i, b := range ciphertext { | |
plaintext[i] = b ^ byte(xc) | |
} | |
return plaintext | |
} | |
type ChannelOptions struct { | |
Key byte // Key is the encryption key | |
} | |
// GetCipher returns the Cipher for o. Returns nil if o is nil or if there is no key | |
func (o *ChannelOptions) GetCipher() Cipher { | |
if o == nil || o.Key == 0 { | |
return nil | |
} | |
return XORCipher(o.Key) | |
} | |
// Message is the base message type. | |
// | |
// JSON payloads | |
// | |
// Message supports encoding payloads as JSON. If Data is any of the following, it'll be encoded | |
// as a JSON string: | |
// - anything implementing json.Marshaler | |
// - map[string]interface{} | |
// - slices of anything except []byte (which are taken to be binary and encoded differently | |
// depending on wire format) | |
type Message struct { | |
ID string `json:"id",msgpack:"id"` | |
Data interface{} `json:"data,omitempty",msgpack:"data,omitempty"` | |
Encoding string `json:"encoding,omitempty",msgpack:"encoding,omitempty"` | |
// TODO: add field Extras JSONObject !! (see TM2i) | |
// ChannelOptions can be embedded to provide payload encryption keys. See WithOpts | |
// | |
// GEOFREY, NOTE: this is embedded so we can have Message be a standard json.Marshaler / | |
// json.Unmarshaler but still have access to ChannelOptions in marshalers/unmarshalers | |
*ChannelOptions `json:"-",msgpack:"-"` | |
} | |
func (m *Message) GoString() string { | |
if m == nil { | |
return "(*Message)(nil)" | |
} | |
return fmt.Sprintf("%#v", *m) | |
} | |
// WithOpts returns a copy of m with channel options added. Used to provide e.g. encryption keys | |
func (m Message) WithOpts(o *ChannelOptions) *Message { | |
m.ChannelOptions = o | |
return &m | |
} | |
// setOpts implements mapConvertible. See its documentation for more information | |
func (m *Message) setOpts(o *ChannelOptions) { | |
m.ChannelOptions = o | |
} | |
// JSONObject is a convenience type alias for map[string]interface{} | |
type JSONObject = map[string]interface{} | |
var ( | |
jsonMarshalerType = reflect.TypeOf(json.Marshaler(nil)) | |
jsonObjType = reflect.TypeOf(JSONObject(nil)) | |
) | |
// maybeJSONEncode will marshal m.Data into a JSON string if it's a type that can be marshaled to | |
// JSON, and adds "json" as an encoding. | |
// Supported types are: | |
// - anything implementing json.Marshaler | |
// - map[string]interface{} | |
// - slices of anything except []byte (which are taken to be binary and handled by the marshaling | |
// methods) | |
// | |
// Modifies m. Does nothing if m.Data's type is none of the above. | |
func (m *Message) maybeJSONEncode() error { | |
dataType := reflect.TypeOf(m.Data) | |
switch dataType { | |
case jsonMarshalerType: | |
bs, err := m.Data.(json.Marshaler).MarshalJSON() | |
if err != nil { | |
return err | |
} | |
m.Data = string(bs) | |
m.addEncoding(EncodingJSON) | |
return nil | |
case jsonObjType: | |
bs, err := json.Marshal(m.Data.(JSONObject)) | |
if err != nil { | |
return err | |
} | |
m.Data = string(bs) | |
m.addEncoding(EncodingJSON) | |
return nil | |
} | |
// marshal any sort of slice except for []byte (i.e. []uint8) | |
if dataType.Kind() == reflect.Slice && dataType.Elem().Kind() != reflect.Uint8 { | |
bs, err := json.Marshal(m.Data) | |
if err != nil { | |
return err | |
} | |
m.Data = string(bs) | |
m.addEncoding(EncodingJSON) | |
} | |
return nil | |
} | |
// encode encodes a message's Data according to its type and whether m has a ChannelOptions with an | |
// encryption key (in this example, Key != 0 means the key is set). | |
// Value receiver, so original m is not modified. | |
func (m Message) encode() (Message, error) { | |
// if payload's JSONable, marshal it here | |
m.maybeJSONEncode() | |
// now we know that Data should be either: | |
// - a string because of JSON conversion above | |
// - a binary | |
// | |
// any other type is invalid according to the spec and we can bail out | |
if _, ok := m.Data.(string); ok { | |
// if Data is a string and we're going to encrypt it, then we need to add utf-8 to the | |
// encoding to signify that the original payload was a string. If Data's a string and we're | |
// not going to be encrypting it, we don't need to mark encoding as utf-8 because strings | |
// can be represented as JSON and msgpack | |
if m.GetCipher() != nil { | |
m.addEncoding(EncodingUTF8) | |
} | |
} else if _, ok := m.Data.([]byte); !ok { | |
return Message{}, errors.New("unsupported payload type") | |
} | |
if cipher := m.GetCipher(); cipher != nil { | |
// since we know that m.Data is either []byte or string at this point, coerceBytes is always | |
// safe here | |
bs, _ := coerceBytes(m.Data) | |
m.Data = cipher.Encrypt(bs) | |
m.addEncoding(EncodingXOR) | |
} | |
return m, nil | |
} | |
type noKeyErr struct{} | |
func (_ noKeyErr) Error() string { | |
return "no encryption key provided, but payload was encrypted" | |
} | |
// IsNoKeyErr returns true if e was caused by trying to unmarshal an encrypted message when no | |
// encryption keys were provided | |
func IsNoKeyErr(e error) bool { | |
_, ok := e.(noKeyErr) | |
return ok | |
} | |
// Decrypt a message's payload using encryption keys set with WithOpts. Returns the decrypted | |
// payload or an error. | |
// | |
// GEOFREY, NOTE: this should probably be exported due to RSL6b (if decryption fails, we'll want the | |
// client to be able to retry it with different options later) | |
func (m Message) Decrypt() (interface{}, error) { | |
cipher := m.GetCipher() | |
if cipher == nil { | |
return nil, noKeyErr{} | |
} | |
dataBs, err := coerceBytes(m.Data) | |
if err != nil { | |
return nil, err | |
} | |
return cipher.Decrypt(dataBs), nil | |
} | |
func (m Message) decode() (Message, error) { | |
encs := strings.Split(m.Encoding, "/") | |
// reverse encs | |
for left, right := 0, len(encs)-1; left < right; left, right = left+1, right-1 { | |
encs[left], encs[right] = encs[right], encs[left] | |
} | |
for _, encoding := range encs { | |
// NOTE: real implementation needs to handle different ciphers, so needs to do parsing on | |
// each "encoding" value similar to how e.g. the Java version does it: | |
// https://github.com/ably/ably-java/blob/bc7d98e048123784d01b08a211ea6785fb40be11/lib/src/main/java/io/ably/lib/types/BaseMessage.java#L71-L120 | |
switch encoding { | |
case EncodingUTF8: | |
var err error | |
m.Data, err = coerceString(m.Data) | |
if err != nil { | |
return m, fmt.Errorf("was supposed to decode UTF-8 next, but payload is currently of type %T (err: %s)", | |
m.Data, err.Error()) | |
} | |
case EncodingJSON: | |
dataBs, err := coerceBytes(m.Data) | |
if err != nil { | |
return m, fmt.Errorf("was supposed to decode JSON next, but payload is currently of type %T: %#v (err: %s)", | |
m.Data, m.Data, err.Error()) | |
} | |
var result interface{} | |
if err := json.Unmarshal(dataBs, &result); err != nil { | |
return m, fmt.Errorf("error unmarshaling JSON payload of type %T: %s", m.Data, err.Error()) | |
} | |
m.Data = result | |
case EncodingXOR: | |
plaintext, err := m.Decrypt() | |
if err != nil { | |
return m, err | |
} | |
m.Data = plaintext | |
case EncodingBase64: | |
dataBs, err := coerceBytes(m.Data) | |
if err != nil { | |
return m, fmt.Errorf("was supposed to decode base64 next, but payload is currently of type %T (err: %s)", | |
m.Data, err.Error()) | |
} | |
dst := make([]byte, base64.StdEncoding.DecodedLen(len(dataBs))) | |
n, err := base64.StdEncoding.Decode(dst, dataBs) | |
if err != nil { | |
return m, fmt.Errorf("error decoding base64: %s", err.Error()) | |
} | |
// NOTE that this potentially wastes DecodedLen-n bytes since the backing array is kept | |
// in memory. | |
// Create a new slice and do a copy() if this is a problem | |
dst = dst[:n] | |
m.Data = dst | |
} | |
} | |
return m, nil | |
} | |
// addEncoding adds encoding to m's Encoding field. NOTE that this modifies m. | |
func (m *Message) addEncoding(encoding string) { | |
if m.Encoding == "" { | |
m.Encoding = encoding | |
} else { | |
m.Encoding = m.Encoding + "/" + encoding | |
} | |
} | |
// mapConvertible is an interface for types that can convert themselves to and from JSONObject. | |
// They must support setting the ChannelOptions via setOpts | |
type mapConvertible interface { | |
toMap() JSONObject | |
fromMap(JSONObject) error | |
// setOpts isn't part of the public API since it doesn't preserve message immutability. | |
// WithOpts is the user-friendly way of doing this | |
setOpts(*ChannelOptions) | |
} | |
// toMap returns a map representation of m | |
func (m Message) toMap() JSONObject { | |
asMap := JSONObject{ | |
"id": m.ID, | |
"data": m.Data, | |
} | |
if enc := m.Encoding; enc != "" { | |
asMap["encoding"] = enc | |
} | |
return asMap | |
} | |
// fromMap takes a map representation of a Message asMap and decodes it into m. Modifies m. | |
func (m *Message) fromMap(asMap JSONObject) error { | |
// TODO: error checking for type assertions | |
m.ID = asMap["id"].(string) | |
if encStr, ok := asMap["encoding"].(string); ok { | |
m.Encoding = encStr | |
} | |
m.Data = asMap["data"] | |
decM, err := m.decode() | |
if err != nil { | |
return err | |
} | |
*m = decM | |
return nil | |
} | |
// encJSON encodes m for JSON transport; it handles binary Data fields with base64 | |
func (m Message) encJSON() (Message, error) { | |
enc, err := m.encode() | |
if err != nil { | |
return Message{}, err | |
} | |
if bs, ok := enc.Data.([]byte); ok { | |
// Data is a byte slice, which we can't represent in JSON, so we need to base64 it | |
b64 := base64.StdEncoding.EncodeToString(bs) | |
enc.Data = b64 | |
// add base64 encoding | |
enc.addEncoding(EncodingBase64) | |
} | |
return enc, nil | |
} | |
// MarshalJSON turns m into JSON. To produce encrypted messages, provide a ChannelOptions with an | |
// encryption key using WithOpts: | |
// Message{Data: "yarr"}.WithOpts(&ChannelOptions{Key: 111}).MarshalJSON() | |
// | |
// If m.Data is a byte slice, will base64 encode it. Does not modify m. | |
func (m Message) MarshalJSON() ([]byte, error) { | |
enc, err := m.encJSON() | |
if err != nil { | |
return nil, err | |
} | |
return json.Marshal(enc.toMap()) | |
} | |
// UnmarshalJSON unmarshals data into m. To unmarshal encrypted messages, provide a ChannelOptions | |
// with an encryption key using WithOpts: | |
// new(Message).WithOpts(&ChannelOptions{Key: 111}).UnmarshalJSON(data) | |
// | |
// JSON payloads | |
// | |
// JSON payloads are unmarshaled to "raw" maps or slices | |
func (m *Message) UnmarshalJSON(data []byte) error { | |
var asMap JSONObject | |
if err := json.Unmarshal(data, &asMap); err != nil { | |
return err | |
} | |
return m.fromMap(asMap) | |
} | |
// MarshalMsgpack turns m into msgpack. To produce encrypted messages, provide a ChannelOptions with an | |
// encryption key using WithOpts (see MarshalJSON) | |
// Does not modify m. | |
func (m Message) MarshalMsgpack() ([]byte, error) { | |
enc, err := m.encode() | |
if err != nil { | |
return nil, err | |
} | |
return msgpack.Marshal(enc.toMap()) | |
} | |
type MockChannel struct { | |
opts *ChannelOptions | |
historyJSON []byte // JSON array of messages in history | |
} | |
func (c *MockChannel) History() (*PaginatedResult, error) { | |
return newPaginatedResult(Message{}, c.opts, c.historyJSON) | |
} | |
// generateMessages generates n test messages with JSON object payload | |
func generateMessages(n int, opts *ChannelOptions) (out []*Message) { | |
for i := 0; i < n; i++ { | |
out = append(out, &Message{ChannelOptions: opts, ID: fmt.Sprintf("id_%d", i), Data: JSONObject{"a": i}}) | |
} | |
return | |
} | |
// newChannelWithHistory creates a mock Channel with nMsgs Messages in history | |
func newChannelWithHistory(nMsgs int, opts *ChannelOptions) *MockChannel { | |
msgs := generateMessages(nMsgs, opts) | |
msgBs, err := json.Marshal(msgs) | |
if err != nil { | |
panic(err) | |
} | |
return &MockChannel{opts: opts, historyJSON: msgBs} | |
} | |
// PaginatedResult. GEOFREY, NOTE: this is not an exact implementation, so use it as a guideline | |
// only | |
type PaginatedResult struct { | |
items []interface{} | |
} | |
// Messages returns the contents of this page of results as a Message slice. Panics if contents | |
// aren't Messages | |
func (pr *PaginatedResult) Messages() []*Message { | |
msgs := make([]*Message, len(pr.items)) | |
for i, item := range pr.items { | |
msgs[i] = item.(*Message) | |
} | |
return msgs | |
} | |
// converObj converts jo into a pointer to msgType | |
func converObj(jo JSONObject, msgType reflect.Type, opts *ChannelOptions) (interface{}, error) { | |
item := reflect.New(msgType).Interface().(mapConvertible) | |
item.setOpts(opts) | |
err := item.fromMap(jo) | |
return item, err | |
} | |
// newPaginatedResult creates a new PaginatedResult. GEOFREY, NOTE: this is not an exact | |
// implementation, so use it as a guideline only | |
func newPaginatedResult(msgType interface{}, opts *ChannelOptions, jsonBytes []byte) (*PaginatedResult, error) { | |
var jsObjs []JSONObject | |
if err := json.Unmarshal(jsonBytes, &jsObjs); err != nil { | |
return nil, err | |
} | |
items := make([]interface{}, len(jsObjs)) | |
for i, jso := range jsObjs { | |
it, err := converObj(jso, reflect.TypeOf(msgType), opts) | |
if err != nil { | |
return nil, err | |
} | |
items[i] = it | |
} | |
// GEOFREY, NOTE: you'll probably need to store the reflect.Type of the message and opts in | |
// PaginatedResult, but omitting those here | |
return &PaginatedResult{items: items}, nil | |
} | |
type PresenceAction int64 | |
const ( | |
PresenceAbsent PresenceAction = iota | |
PresencePresent | |
PresenceEnter | |
PresenceLeave | |
PresenceUpdate | |
) | |
type PresenceMessage struct { | |
Message // embed a Message so we can use shared encoding methods | |
Action PresenceAction `json:"action,omitempty",msgpack:"action,omitempty"` | |
ClientID string `json:"clientId,omitempty",msgpack:"clientId,omitempty"` | |
ConnectionID string `json:"connectionId,omitempty",msgpack:"connectionId,omitempty"` | |
} | |
// WithOpts like in Message. NOTE that this has to be "reimplemented" here so we return a | |
// PresenceMessage instead of a Message | |
func (pm PresenceMessage) WithOpts(o *ChannelOptions) *PresenceMessage { | |
pm.ChannelOptions = o | |
return &pm | |
} | |
// setOpts implements mapConvertible. See its documentation for more information | |
func (pm *PresenceMessage) setOpts(o *ChannelOptions) { | |
pm.ChannelOptions = o | |
} | |
func (pm PresenceMessage) MarshalJSON() ([]byte, error) { | |
enc, err := pm.encJSON() | |
if err != nil { | |
return nil, err | |
} | |
m := enc.toMap() | |
m["action"] = pm.Action | |
m["clientId"] = pm.ClientID | |
m["connectionId"] = pm.ConnectionID | |
return json.Marshal(m) | |
} | |
// UnmarshalJSON unmarshals data into m. To unmarshal encrypted messages, provide a ChannelOptions | |
// with an encryption key using WithOpts: | |
// new(Message).WithOpts(&ChannelOptions{Key: 111}).UnmarshalJSON(data) | |
// | |
// JSON payloads | |
// | |
// JSON payloads are unmarshaled to "raw" maps or slices | |
func (pm *PresenceMessage) UnmarshalJSON(data []byte) error { | |
var asMap JSONObject | |
dec := json.NewDecoder(bytes.NewBuffer(data)) | |
dec.UseNumber() | |
if err := dec.Decode(&asMap); err != nil { | |
return err | |
} | |
return pm.fromMap(asMap) | |
} | |
// fromMap takes a map representation of a PresenceMessage asMap and decodes it into pm. Modifies pm. | |
func (pm *PresenceMessage) fromMap(asMap JSONObject) error { | |
if err := pm.Message.fromMap(asMap); err != nil { | |
return err | |
} | |
// TODO: error checking for type assertions | |
actNum := asMap["action"].(json.Number) | |
act, err := actNum.Int64() | |
if err != nil { | |
return err | |
} | |
pm.Action = PresenceAction(act) | |
pm.ClientID = asMap["clientId"].(string) | |
pm.ConnectionID = asMap["connectionId"].(string) | |
return nil | |
} | |
// toMap returns a map representation of pm | |
func (pm PresenceMessage) toMap() JSONObject { | |
asMap := pm.Message.toMap() | |
asMap["action"] = pm.Action | |
asMap["clientId"] = pm.ClientID | |
asMap["connectionId"] = pm.ConnectionID | |
return asMap | |
} | |
func coerceString(i interface{}) (string, error) { | |
switch v := i.(type) { | |
case []byte: | |
return string(v), nil | |
case string: | |
return v, nil | |
default: | |
return "", fmt.Errorf("UTF8 encoding can only handle types string or []byte, but got %T", i) | |
} | |
} | |
func coerceBytes(i interface{}) ([]byte, error) { | |
switch v := i.(type) { | |
case []byte: | |
return v, nil | |
case string: | |
return []byte(v), nil | |
default: | |
return nil, fmt.Errorf("coerceBytes should never get anything but strings or []byte, got %T", i) | |
} | |
} | |
func mustMsg(m Message, err error) Message { | |
if err != nil { | |
panic(err) | |
} | |
return m | |
} | |
func main() { | |
chanOpts := &ChannelOptions{Key: 111} | |
fmt.Printf("\n\n--------- Encoding and marshaling JSON\n\n") | |
mStrNoEncr := &Message{ | |
ID: "my ID", | |
Data: "bler", | |
} | |
fmt.Printf("A message with string data and no encryption has the encoding '%s'\n", | |
mustMsg(mStrNoEncr.encode()).Encoding) | |
js, _ := mStrNoEncr.MarshalJSON() | |
fmt.Printf("A message with string data and no encryption produces this JSON:\n%s\n\n", string(js)) | |
mStrEncr := &Message{ | |
ID: "my ID", | |
Data: "bler", | |
} | |
fmt.Printf("A message with string data and encryption has the encoding '%s'\n", | |
mustMsg(mStrEncr.WithOpts(chanOpts).encode()).Encoding) | |
js, _ = mStrEncr.WithOpts(chanOpts).MarshalJSON() | |
fmt.Printf("A message with string data and encryption produces this JSON (note that the "+ | |
"encoding field is different!):\n%s\n\n", string(js)) | |
mBytesNoEncr := &Message{ | |
ID: "my id", | |
Data: []byte{1, 2, 3, 4,}, | |
} | |
fmt.Printf("A message with binary data and no encryption has the encoding '%s'\n", | |
mustMsg(mBytesNoEncr.encode()).Encoding) | |
js, _ = mBytesNoEncr.MarshalJSON() | |
fmt.Printf("A message with binary data and no encryption produces this JSON (note that the "+ | |
"encoding field is different!):\n%s\n\n", string(js)) | |
mBytesEncr := &Message{ | |
ID: "my id", | |
Data: []byte{1, 2, 3, 4,}, | |
} | |
fmt.Printf("A message with binary data and encryption has the encoding '%s'\n", | |
mustMsg(mBytesEncr.WithOpts(chanOpts).encode()).Encoding) | |
js, _ = mBytesEncr.WithOpts(chanOpts).MarshalJSON() | |
fmt.Printf("A message with binary data and encryption produces this JSON (note the encoding field!):\n%s\n\n", string(js)) | |
mJSONNoEncr := &Message{ | |
ID: "my ID", | |
Data: JSONObject{"a": 1, "b": true}, | |
} | |
fmt.Printf("A message with a JSON object payload and no encryption has the encoding '%s'\n", | |
mustMsg(mJSONNoEncr.encode()).Encoding) | |
js, _ = mJSONNoEncr.MarshalJSON() | |
fmt.Printf("A message with a JSON object payload and no encryption produces this JSON:\n%s\n\n", string(js)) | |
mJSONEncr := &Message{ | |
ID: "my ID", | |
Data: JSONObject{"a": 1, "b": true}, | |
} | |
fmt.Printf("A message with a JSON object payload and encryption has the encoding '%s'\n", | |
mustMsg(mJSONEncr.WithOpts(chanOpts).encode()).Encoding) | |
js, _ = mJSONEncr.WithOpts(chanOpts).MarshalJSON() | |
fmt.Printf("A message with a JSON object payload and encryption produces this JSON (note the encoding field!):\n%s\n\n", string(js)) | |
mJSONArrNoEncr := &Message{ | |
ID: "my ID", | |
Data: []string{"a", "b"}, | |
} | |
fmt.Printf("A message with JSON array data and no encryption has the encoding '%s'\n", | |
mustMsg(mJSONArrNoEncr.encode()).Encoding) | |
js, _ = mJSONArrNoEncr.MarshalJSON() | |
fmt.Printf("A message with JSON array data and no encryption produces this JSON (note the encoding field!):\n%s\n\n", string(js)) | |
mPresEncr := &PresenceMessage{ | |
Action: PresenceLeave, | |
ConnectionID: "zot", | |
ClientID: "bleb", | |
} | |
mPresEncr.ID = "my ID" | |
mPresEncr.Data = []byte{1, 2, 3} | |
fmt.Printf("A presence message with binary data and encryption has the encoding '%s'\n", | |
mustMsg(mPresEncr.WithOpts(chanOpts).encode()).Encoding) | |
js, _ = mPresEncr.WithOpts(chanOpts).MarshalJSON() | |
fmt.Printf("A presence message with bbinary data and encryption produces this JSON (note the encoding field!):\n%s\n\n", js) | |
showcaseUnmarshal() | |
showcasePaginated() | |
} | |
func showcaseUnmarshal() { | |
chanOpts := &ChannelOptions{Key: 111} | |
fmt.Printf("\n\n--------- Unmarshaling JSON\n\n") | |
var ( | |
m *Message | |
unmErr error | |
) | |
mJSONEncr := &Message{ | |
ID: "my ID", | |
Data: JSONObject{"a": 1, "b": true}, | |
} | |
jsObj, _ := mJSONEncr.WithOpts(chanOpts).MarshalJSON() | |
m = new(Message).WithOpts(chanOpts) | |
unmErr = m.UnmarshalJSON(jsObj) | |
if unmErr != nil { | |
panic(unmErr) | |
} | |
fmt.Printf("An encrypted message with JSON object payload has the JSON:\n%s\nIt is unmarshaled to\n%#v\n\n", jsObj, m) | |
mJSONArrNoEncr := &Message{ | |
ID: "my ID", | |
Data: []string{"a", "b"}, | |
} | |
jsArr, _ := mJSONArrNoEncr.MarshalJSON() | |
m = new(Message).WithOpts(chanOpts) | |
unmErr = m.UnmarshalJSON(jsArr) | |
if unmErr != nil { | |
panic(unmErr) | |
} | |
fmt.Printf("An unencrypted message with JSON array payload has the JSON:\n%s\nIt is unmarshaled to\n%#v\n\n", jsArr, m) | |
mPres := &PresenceMessage{ | |
Action: PresenceLeave, | |
ConnectionID: "zot", | |
ClientID: "bleb", | |
} | |
mPres.ID = "my ID" | |
mPres.Data = []string{"1", "2", "3"} | |
jsPres, _ := mPres.MarshalJSON() | |
pm := new(PresenceMessage) | |
unmErr = pm.UnmarshalJSON(jsPres) | |
if unmErr != nil { | |
panic(unmErr) | |
} | |
fmt.Printf("An unencrypted presence message with int slice payload has the JSON:\n%s\nIt is unmarshaled to\n%#v\n\n", jsPres, pm) | |
} | |
func showcasePaginated() { | |
fmt.Printf("\n\n--------- PaginatedResults \n\n") | |
chanOpts := &ChannelOptions{Key: 111} | |
ch := newChannelWithHistory(4, chanOpts) | |
fmt.Printf("Generated an encrypted mock channel that has a message history:\n%s\n\n", string(ch.historyJSON)) | |
hist, err := ch.History() | |
if err != nil { | |
panic(err) | |
} | |
msgs := hist.Messages() | |
fmt.Printf("Paginated result returns these messages:\n%#v\n", msgs) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Output: