first commit - still working on tests for edge cases
This commit is contained in:
commit
f20fcb466d
2
README.md
Normal file
2
README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# msmsg
|
||||
msmsg (Mad Scienstist Messaging Protocol) is a simple protocol for multiplexing, as specified by Pete McNeil. See https://www.lifeatwarp9.com/2016/02/channels-a-friendly-protocol-for-multiplexing-ipc/ for the full specs.
|
||||
207
message.go
Normal file
207
message.go
Normal file
@ -0,0 +1,207 @@
|
||||
package msmsg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Dialect string
|
||||
ID string
|
||||
Parameters map[string]string
|
||||
Body string
|
||||
ClosesChannel bool
|
||||
}
|
||||
|
||||
func (m Message) String() string {
|
||||
switch {
|
||||
case m.Chatter():
|
||||
return m.Body
|
||||
}
|
||||
var builder strings.Builder
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func ByteIsSpace(b byte) bool {
|
||||
return b == ' ' ||
|
||||
b == '\t' ||
|
||||
b == '\r' ||
|
||||
b == '\n'
|
||||
}
|
||||
|
||||
// Lines that are not encapsulated in XML tags or
|
||||
// are not recognized as part of a known dialect
|
||||
// are called chatter.
|
||||
func (m *Message) Chatter() bool {
|
||||
return len(m.Dialect) == 0 && len(m.ID) == 0 && len(m.Parameters) == 0
|
||||
}
|
||||
|
||||
// Lines that have no associated ChannelID are
|
||||
// called anonymous messages.
|
||||
func (m *Message) Anonymous() bool {
|
||||
return !m.Chatter() && len(m.ID) == 0
|
||||
}
|
||||
|
||||
type token struct {
|
||||
dialect string
|
||||
id string
|
||||
parameters map[string]string
|
||||
body string
|
||||
closesMessage bool
|
||||
closesChannel bool
|
||||
}
|
||||
|
||||
var (
|
||||
errTokenMalformed = errors.New("token is malformed")
|
||||
)
|
||||
|
||||
func parseToken(s string) (token, error) {
|
||||
var results token
|
||||
|
||||
if !strings.HasPrefix(s, "<") || !strings.HasSuffix(s, ">") {
|
||||
return token{body: s}, fmt.Errorf("%w: missing opening and/or closing angle bracket", errTokenMalformed)
|
||||
}
|
||||
|
||||
buffer := strings.TrimSpace(s[1 : len(s)-1])
|
||||
if strings.HasPrefix(buffer, "/") {
|
||||
results.closesMessage = true
|
||||
buffer = strings.TrimSpace(buffer[1:])
|
||||
}
|
||||
if strings.HasSuffix(buffer, "/") {
|
||||
results.closesMessage = true
|
||||
results.closesChannel = true
|
||||
buffer = strings.TrimSpace(buffer[:len(buffer)-1])
|
||||
}
|
||||
|
||||
// try to grab the first word off the front, this should be the dialect
|
||||
i := strings.IndexAny(buffer, " \t\r\n")
|
||||
if i == -1 {
|
||||
results.dialect = buffer
|
||||
return results, nil
|
||||
} else if i < len(buffer) {
|
||||
// is the next non-whitespace char an '='?
|
||||
for j := i + 1; j < len(buffer); j++ {
|
||||
if ByteIsSpace(buffer[j]) {
|
||||
i = j
|
||||
break
|
||||
} else if buffer[j] == '=' {
|
||||
i = 0
|
||||
break
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i > 0 {
|
||||
results.dialect = buffer[:i]
|
||||
buffer = strings.TrimSpace(buffer[i:])
|
||||
}
|
||||
}
|
||||
|
||||
// everything else is presumed to be key=value pairs
|
||||
type STATE int8
|
||||
const (
|
||||
KEY STATE = iota
|
||||
VALUE
|
||||
)
|
||||
var key, value strings.Builder
|
||||
var quote byte
|
||||
var state STATE
|
||||
for i = 0; i < len(buffer); i++ {
|
||||
theChar := buffer[i]
|
||||
switch state {
|
||||
case KEY:
|
||||
// parse the key and prepare to parse the value by finding the '=' and the quotation mark
|
||||
if ByteIsSpace(theChar) {
|
||||
continue
|
||||
} else if theChar == '=' {
|
||||
// find the next quotation mark
|
||||
for i = i + 1; i < len(buffer); i++ {
|
||||
theChar = buffer[i]
|
||||
if ByteIsSpace(theChar) {
|
||||
continue
|
||||
} else if theChar == '\'' || theChar == '"' {
|
||||
quote = theChar
|
||||
state = VALUE
|
||||
break
|
||||
} else {
|
||||
return token{body: s}, fmt.Errorf("%w: parameter value missing quotation mark", errTokenMalformed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
key.WriteByte(theChar)
|
||||
}
|
||||
case VALUE:
|
||||
// if quote == \0 something bad has happened - we don't know what we're looking for
|
||||
if quote == '\x00' {
|
||||
return token{body: s}, fmt.Errorf("%w: missing quotation mark in VALUE state...", errTokenMalformed)
|
||||
} else if theChar == quote {
|
||||
// we have a key and a value - add them to the parameters
|
||||
if results.parameters == nil {
|
||||
results.parameters = make(map[string]string, 1)
|
||||
}
|
||||
results.parameters[key.String()] = value.String()
|
||||
key.Reset()
|
||||
value.Reset()
|
||||
quote = '\x00'
|
||||
state = KEY
|
||||
continue
|
||||
} else {
|
||||
value.WriteByte(theChar)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// <dialect ch='0'>This message is opening this channel.</dialect>
|
||||
|
||||
func ParseMessage(s string) Message {
|
||||
var message Message
|
||||
if !strings.HasPrefix(s, "<") || !strings.HasSuffix(s, ">") {
|
||||
message.Body = s
|
||||
}
|
||||
headerEnd := strings.IndexByte(s, '>')
|
||||
headerString := s[:headerEnd+1]
|
||||
header, err := parseToken(headerString)
|
||||
if err != nil {
|
||||
return Message{Body: s}
|
||||
}
|
||||
message.Dialect = strings.Clone(header.dialect)
|
||||
for k, v := range header.parameters {
|
||||
switch strings.ToLower(k) {
|
||||
case "ch":
|
||||
message.ID = strings.Clone(v)
|
||||
default:
|
||||
if message.Parameters == nil {
|
||||
message.Parameters = make(map[string]string, len(header.parameters))
|
||||
}
|
||||
message.Parameters[strings.Clone(k)] = strings.Clone(v)
|
||||
}
|
||||
}
|
||||
// is there a footer?
|
||||
if headerEnd == len(s)-1 {
|
||||
if header.closesChannel {
|
||||
message.ClosesChannel = true
|
||||
}
|
||||
return message
|
||||
}
|
||||
footerStart := strings.LastIndexByte(s, '<')
|
||||
if footerStart <= 0 {
|
||||
// malformed
|
||||
return Message{Body: s}
|
||||
}
|
||||
footer, err := parseToken(s[footerStart:])
|
||||
if err != nil {
|
||||
return Message{Body: s}
|
||||
}
|
||||
if want, got := header.dialect, footer.dialect; want != got {
|
||||
return Message{Body: s}
|
||||
} else if !footer.closesMessage {
|
||||
return Message{Body: s}
|
||||
}
|
||||
// the stuff between is the body
|
||||
message.Body = strings.TrimSpace(s[headerEnd+1 : footerStart])
|
||||
return message
|
||||
}
|
||||
60
message_test.go
Normal file
60
message_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
package msmsg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var TheTestObjects = map[string]Message{
|
||||
"<dialect ch= '0'>This message is opening this channel.</dialect>": {
|
||||
Dialect: "dialect",
|
||||
ID: "0",
|
||||
Body: "This message is opening this channel.",
|
||||
},
|
||||
"<dialect ch ='0'></dialect>": {
|
||||
Dialect: "dialect",
|
||||
ID: "0",
|
||||
},
|
||||
"<dialect ch='0'/>": {
|
||||
Dialect: "dialect",
|
||||
ID: "0",
|
||||
ClosesChannel: true,
|
||||
},
|
||||
"<dialect ch = '0' ctrl='shutdown' />": {
|
||||
Dialect: "dialect",
|
||||
ID: "0",
|
||||
ClosesChannel: true,
|
||||
Parameters: map[string]string{
|
||||
"ctrl": "shutdown",
|
||||
},
|
||||
},
|
||||
"<dialect>Hello</dialect>": {Dialect: "dialect", Body: "Hello"},
|
||||
|
||||
"this is chatter": {
|
||||
Body: "this is chatter",
|
||||
},
|
||||
// these are malformed
|
||||
"<dialect ch=10 />": {Body: "<dialect ch=10 />"},
|
||||
"<dia": {Body: "<dia"},
|
||||
"<dialect>Hello": {Body: "<dialect>Hello"},
|
||||
"<dialect>Hello<dialect>": {Body: "<dialect>Hello<dialect>"},
|
||||
"<dialect>Hello<dict>": {Body: "<dialect>Hello<dict>"},
|
||||
"<dialect>Hello<dial ect>": {Body: "<dialect>Hello<dial ect>"},
|
||||
}
|
||||
|
||||
func TestParseMessage(t *testing.T) {
|
||||
for input, wantOutput := range TheTestObjects {
|
||||
gotOutput := ParseMessage(input)
|
||||
if want, got := wantOutput.Dialect, gotOutput.Dialect; want != got {
|
||||
t.Fatalf("error parsing %s: wanted dialect %q; got %q\n", input, want, got)
|
||||
} else if want, got := wantOutput.ID, gotOutput.ID; want != got {
|
||||
t.Fatalf("error parsing %s: wanted ID %q; got %q\n", input, want, got)
|
||||
} else if want, got := wantOutput.Body, gotOutput.Body; want != got {
|
||||
t.Fatalf("error parsing %s: wanted body %q; got %q\n", input, want, got)
|
||||
} else if !reflect.DeepEqual(wantOutput.Parameters, gotOutput.Parameters) {
|
||||
t.Fatalf("error parsing %s: wanted parameters %+v; got %+v\n", input, wantOutput.Parameters, gotOutput.Parameters)
|
||||
} else if want, got := wantOutput.ClosesChannel, gotOutput.ClosesChannel; want != got {
|
||||
t.Fatalf("error parsing %s: wanted ClosesChannel=%v; got %v\n", input, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user