Repository: FluuxIO/go-xmpp
Pull request #65 (2019-06-21 14:39:47)

Add StanzaType matcher
This allows to express route matching on IQ, with a specific namespace and packet type for example.

You can have a different function to handle set, get and error for example.

I also added a test showing that an empty route can act as a catch all matcher.


Patch:
diff --git a/router.go b/router.go
index 5b4515b..5264a95 100644
--- a/router.go
+++ b/router.go
@@ -12,6 +12,9 @@ Here are important rules to keep in mind while setting your routes and matchers:
 - Routes are evaluated in the order they are set.
 - When a route matches, it is executed and all others routes are ignored. For each packet, only a single
   route is executed.
+- An empty route will match everything. Adding an empty route as the last route in your router will
+  allow you to get all stanzas that did not match any previous route. You can for example use this to
+  log all unexpected stanza received by your client or component.
 
 TODO: Automatically reply to IQ that do not match any route, to comply to XMPP standard.
 */
@@ -145,6 +148,41 @@ func (r *Route) Packet(name string) *Route {
 	return r.addMatcher(nameMatcher(name))
 }
 
+// -------------------------
+// Match on stanza type
+
+// nsTypeMather matches on a list of IQ  payload namespaces
+type nsTypeMatcher []string
+
+func (m nsTypeMatcher) Match(p Packet, match *RouteMatch) bool {
+	// TODO: Rework after merge of #62
+	var stanzaType string
+	switch packet := p.(type) {
+	case IQ:
+		stanzaType = packet.Type
+	case Presence:
+		stanzaType = packet.Type
+	case Message:
+		if packet.Type == "" {
+			// optional on message, normal is the default type
+			stanzaType = "normal"
+		} else {
+			stanzaType = packet.Type
+		}
+	default:
+		return false
+	}
+	return matchInArray(m, stanzaType)
+}
+
+// IQNamespaces adds an IQ matcher, expecting both an IQ and a
+func (r *Route) StanzaType(types ...string) *Route {
+	for k, v := range types {
+		types[k] = strings.ToLower(v)
+	}
+	return r.addMatcher(nsTypeMatcher(types))
+}
+
 // -------------------------
 // Match on IQ and namespace
 
@@ -152,7 +190,6 @@ func (r *Route) Packet(name string) *Route {
 type nsIQMatcher []string
 
 func (m nsIQMatcher) Match(p Packet, match *RouteMatch) bool {
-	// TODO
 	iq, ok := p.(IQ)
 	if !ok {
 		return false


Test patch:
diff --git a/router_test.go b/router_test.go
index 4e44f41..4b6efe6 100644
--- a/router_test.go
+++ b/router_test.go
@@ -8,9 +8,6 @@ import (
 	"gosrc.io/xmpp"
 )
 
-// ============================================================================
-// SenderMock
-
 // ============================================================================
 // Test route & matchers
 
@@ -51,7 +48,6 @@ func TestIQNSMatcher(t *testing.T) {
 	// Check that an IQ with proper namespace does match
 	conn := NewSenderMock()
 	iqDisco := xmpp.NewIQ("get", "", "localhost", "1", "")
-	// TODO: Add a function to generate payload with proper namespace initialisation
 	iqDisco.Payload = &xmpp.DiscoInfo{
 		XMLName: xml.Name{
 			Space: xmpp.NSDiscoInfo,
@@ -65,7 +61,6 @@ func TestIQNSMatcher(t *testing.T) {
 	// Check that another namespace is not matched
 	conn = NewSenderMock()
 	iqVersion := xmpp.NewIQ("get", "", "localhost", "1", "")
-	// TODO: Add a function to generate payload with proper namespace initialisation
 	iqVersion.Payload = &xmpp.DiscoInfo{
 		XMLName: xml.Name{
 			Space: "jabber:iq:version",
@@ -77,6 +72,144 @@ func TestIQNSMatcher(t *testing.T) {
 	}
 }
 
+func TestTypeMatcher(t *testing.T) {
+	router := xmpp.NewRouter()
+	router.NewRoute().
+		StanzaType("normal").
+		HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
+			_ = s.SendRaw(successFlag)
+		})
+
+	// Check that a packet with the proper type matches
+	conn := NewSenderMock()
+	message := xmpp.NewMessage("normal", "", "test@localhost", "1", "")
+	message.Body = "hello"
+	router.Route(conn, message)
+
+	if conn.String() != successFlag {
+		t.Errorf("'normal' message should have been matched and routed: %v", message)
+	}
+
+	// We should match on default type 'normal' for message without a type
+	conn = NewSenderMock()
+	message = xmpp.NewMessage("", "", "test@localhost", "1", "")
+	message.Body = "hello"
+	router.Route(conn, message)
+
+	if conn.String() != successFlag {
+		t.Errorf("message should have been matched and routed: %v", message)
+	}
+
+	// We do not match on other types
+	conn = NewSenderMock()
+	iqVersion := xmpp.NewIQ("get", "service.localhost", "test@localhost", "1", "")
+	iqVersion.Payload = &xmpp.DiscoInfo{
+		XMLName: xml.Name{
+			Space: "jabber:iq:version",
+			Local: "query",
+		}}
+	router.Route(conn, iqVersion)
+
+	if conn.String() == successFlag {
+		t.Errorf("iq get should not have been matched and routed: %v", iqVersion)
+	}
+}
+
+func TestCompositeMatcher(t *testing.T) {
+	router := xmpp.NewRouter()
+	router.NewRoute().
+		IQNamespaces("jabber:iq:version").
+		StanzaType("get").
+		HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
+			_ = s.SendRaw(successFlag)
+		})
+
+	// Data set
+	getVersionIq := xmpp.NewIQ("get", "service.localhost", "test@localhost", "1", "")
+	getVersionIq.Payload = &xmpp.Version{
+		XMLName: xml.Name{
+			Space: "jabber:iq:version",
+			Local: "query",
+		}}
+
+	setVersionIq := xmpp.NewIQ("set", "service.localhost", "test@localhost", "1", "")
+	setVersionIq.Payload = &xmpp.Version{
+		XMLName: xml.Name{
+			Space: "jabber:iq:version",
+			Local: "query",
+		}}
+
+	GetDiscoIq := xmpp.NewIQ("get", "service.localhost", "test@localhost", "1", "")
+	GetDiscoIq.Payload = &xmpp.DiscoInfo{
+		XMLName: xml.Name{
+			Space: "http://jabber.org/protocol/disco#info",
+			Local: "query",
+		}}
+
+	message := xmpp.NewMessage("normal", "", "test@localhost", "1", "")
+	message.Body = "hello"
+
+	tests := []struct {
+		name  string
+		input xmpp.Packet
+		want  bool
+	}{
+		{name: "match get version iq", input: getVersionIq, want: true},
+		{name: "ignore set version iq", input: setVersionIq, want: false},
+		{name: "ignore get discoinfo iq", input: GetDiscoIq, want: false},
+		{name: "ignore message", input: message, want: false},
+	}
+
+	//
+	for _, tc := range tests {
+		t.Run(tc.name, func(st *testing.T) {
+			conn := NewSenderMock()
+			router.Route(conn, tc.input)
+
+			res := conn.String() == successFlag
+			if tc.want != res {
+				st.Errorf("incorrect result for %#v\nMatch = %#v, expecting %#v", tc.input, res, tc.want)
+			}
+		})
+	}
+}
+
+// A blank route with empty matcher will always match
+// It can be use to receive all packets that do not match any of the previous route.
+func TestCatchallMatcher(t *testing.T) {
+	router := xmpp.NewRouter()
+	router.NewRoute().
+		HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
+			_ = s.SendRaw(successFlag)
+		})
+
+	// Check that we match on several packets
+	conn := NewSenderMock()
+	message := xmpp.NewMessage("chat", "", "test@localhost", "1", "")
+	message.Body = "hello"
+	router.Route(conn, message)
+
+	if conn.String() != successFlag {
+		t.Errorf("chat message should have been matched and routed: %v", message)
+	}
+
+	conn = NewSenderMock()
+	iqVersion := xmpp.NewIQ("get", "service.localhost", "test@localhost", "1", "")
+	iqVersion.Payload = &xmpp.DiscoInfo{
+		XMLName: xml.Name{
+			Space: "jabber:iq:version",
+			Local: "query",
+		}}
+	router.Route(conn, iqVersion)
+
+	if conn.String() != successFlag {
+		t.Errorf("iq get should have been matched and routed: %v", iqVersion)
+	}
+}
+
+// ============================================================================
+// SenderMock
+
 var successFlag = "matched"
 
 type SenderMock struct {