Use an approach to build stanza that do not require a "builder" abstraction

This commit is contained in:
Mickael Remond 2019-06-27 14:30:23 +02:00
parent 1dacc663d3
commit 20a66dc47d
No known key found for this signature in database
GPG Key ID: E6F6045D79965AA3
7 changed files with 159 additions and 100 deletions

View File

@ -110,7 +110,7 @@ func discoInfoRoot(iqResp *stanza.IQ, opts xmpp.ComponentOptions) {
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: identity,
Identity: []stanza.Identity{identity},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
@ -148,7 +148,7 @@ func discoInfoPEP(iqResp *stanza.IQ) {
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: identity,
Identity: []stanza.Identity{identity},
Node: pepNode,
Features: []stanza.Feature{
{Var: "http://jabber.org/protocol/pubsub#access-presence"},

View File

@ -8,9 +8,10 @@ When creating stanzas, you can use two approaches:
1. You can create IQ, Presence or Message structs, set the fields and manually prepare extensions struct to add to the
stanza.
2. You can use `stanza` Builder providing
2. You can use `stanza` build helper to be guided when creating the stanza, and have more controls performed on the
final stanza.
The methods are equivalent and you can use whatever suits you best. The Builder will generate the same type of
The methods are equivalent and you can use whatever suits you best. The helpers will finally generate the same type of
struct that you can build by hand.
### Composing stanzas manually with structs
@ -28,7 +29,7 @@ Here is for example how you would generate an IQ discovery result:
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: identity,
Identity: []stanza.Identity{identity},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
@ -38,19 +39,14 @@ Here is for example how you would generate an IQ discovery result:
}
iqResp.Payload = &payload
### Using Builder
### Using helpers
Here is for example how you would generate an IQ discovery result using Builder:
b := stanza.NewBuilder()
iq := b.IQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
payload := b.DiscoInfo()
identity := b.Identity("Test Component", "gateway", "service")
payload.SetFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1").
SetIdentities(identity)
iq.Payload = payload
iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
disco := iq.DiscoInfo()
disco.AddIdentity("Test Component", "gateway", "service")
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
## Payload and extensions

View File

@ -1,62 +0,0 @@
package stanza
import (
"encoding/xml"
)
type builder struct{ lang string }
// NewBuilder create a builder structure. It act as an interface for packet generation.
// The goal is to work well with code completion to more easily.
//
// Using the builder to format and create packets is optional. You can always prepare
// your packet dealing with the struct manually and initializing them with the right values.
func NewBuilder() *builder {
return &builder{}
}
// Set default language
func (b *builder) Lang(lang string) *builder {
b.lang = lang
return b
}
func (b *builder) IQ(a Attrs) IQ {
return IQ{
XMLName: xml.Name{Local: "iq"},
Attrs: a,
}
}
func (b *builder) Message(a Attrs) Message {
return Message{
XMLName: xml.Name{Local: "message"},
Attrs: a,
}
}
func (b *builder) Presence(a Attrs) Presence {
return Presence{
XMLName: xml.Name{Local: "presence"},
Attrs: a,
}
}
// ======================================================================================
// IQ payloads
// DiscoInfo builds a default DiscoInfo payload
func (*builder) DiscoInfo() *DiscoInfo {
d := DiscoInfo{
XMLName: xml.Name{
Space: NSDiscoInfo,
Local: "query",
},
}
return &d
}
// Identity builds a identity struct for use in Disco
func (*builder) Identity(name, category, typ string) *Identity {
return &Identity{}
}

View File

@ -1,20 +1,23 @@
package stanza
import "encoding/xml"
import (
"encoding/xml"
)
// ============================================================================
// Disco
// Disco Info
const (
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
NSDiscoItems = "http://jabber.org/protocol/disco#items"
)
// Disco Info
// ----------
// Namespaces
type DiscoInfo struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"`
Node string `xml:"node,attr,omitempty"`
Identity Identity `xml:"identity"`
Identity []Identity `xml:"identity"`
Features []Feature `xml:"feature"`
}
@ -22,6 +25,56 @@ func (d *DiscoInfo) Namespace() string {
return d.XMLName.Space
}
// ---------------
// Builder helpers
// DiscoInfo builds a default DiscoInfo payload
func (iq *IQ) DiscoInfo() *DiscoInfo {
d := DiscoInfo{
XMLName: xml.Name{
Space: NSDiscoInfo,
Local: "query",
},
}
iq.Payload = &d
return &d
}
func (d *DiscoInfo) AddIdentity(name, category, typ string) {
identity := Identity{
XMLName: xml.Name{Local: "identity"},
Name: name,
Category: category,
Type: typ,
}
d.Identity = append(d.Identity, identity)
}
func (d *DiscoInfo) AddFeatures(namespace ...string) {
for _, ns := range namespace {
d.Features = append(d.Features, Feature{Var: ns})
}
}
func (d *DiscoInfo) SetNode(node string) {
d.Node = node
}
func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo {
d.Identity = ident
return d
}
func (d *DiscoInfo) SetFeatures(namespace ...string) *DiscoInfo {
for _, ns := range namespace {
d.Features = append(d.Features, Feature{Var: ns})
}
return d
}
// -----------
// SubElements
type Identity struct {
XMLName xml.Name `xml:"identity,omitempty"`
Name string `xml:"name,attr,omitempty"`
@ -34,7 +87,13 @@ type Feature struct {
Var string `xml:"var,attr"`
}
// Disco Items
// ============================================================================
// Disco Info
const (
NSDiscoItems = "http://jabber.org/protocol/disco#items"
)
type DiscoItems struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
Node string `xml:"node,attr,omitempty"`

55
stanza/iq_disco_test.go Normal file
View File

@ -0,0 +1,55 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
func TestDiscoInfoBuilder(t *testing.T) {
iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
disco := iq.DiscoInfo()
disco.AddIdentity("Test Component", "gateway", "service")
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
// Marshall
data, err := xml.Marshal(iq)
if err != nil {
t.Errorf("cannot marshal xml structure: %s", err)
return
}
// Unmarshall
var parsedIQ stanza.IQ
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error: %s", data, err)
}
// Check result
pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo)
if !ok {
t.Errorf("Parsed stanza does not contain an IQ payload")
}
// Check features
features := []string{stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1"}
if len(pp.Features) != len(features) {
t.Errorf("Features length mismatch: %#v", pp.Features)
} else {
for i, f := range pp.Features {
if f.Var != features[i] {
t.Errorf("Missing feature: %s", features[i])
}
}
}
// Check identity
if len(pp.Identity) != 1 {
t.Errorf("Identity length mismatch: %#v", pp.Identity)
} else {
if pp.Identity[0].Name != "Test Component" {
t.Errorf("Incorrect identity name: %#v", pp.Identity[0].Name)
}
}
}

View File

@ -37,11 +37,11 @@ func TestUnmarshalIqs(t *testing.T) {
func TestGenerateIq(t *testing.T) {
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
payload := stanza.DiscoInfo{
Identity: stanza.Identity{
Name: "Test Gateway",
Identity: []stanza.Identity{
{Name: "Test Gateway",
Category: "gateway",
Type: "mqtt",
},
}},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
@ -63,8 +63,8 @@ func TestGenerateIq(t *testing.T) {
t.Errorf("Unmarshal(%s) returned error", data)
}
if !xmlEqual(parsedIQ.Payload, iq.Payload) {
t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ.Payload, iq.Payload))
if !xmlEqual(iq.Payload, parsedIQ.Payload) {
t.Errorf("non matching items\n%s", xmlDiff(iq.Payload, parsedIQ.Payload))
}
}

View File

@ -10,6 +10,15 @@ import (
// marshal / unmarshal. There is no need to manage them on the manually
// crafted structure.
func xmlEqual(x, y interface{}) bool {
return cmp.Equal(x, y, xmlOpts())
}
// xmlDiff compares xml structures ignoring namespace preferences
func xmlDiff(x, y interface{}) string {
return cmp.Diff(x, y, xmlOpts())
}
func xmlOpts() cmp.Options {
alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true })
opts := cmp.Options{
cmp.FilterValues(func(x, y interface{}) bool {
@ -20,10 +29,12 @@ func xmlEqual(x, y interface{}) bool {
if xx == zero || yy == zero {
return true
}
if xx.Space == "" || yy.Space == "" {
return true
}
}
return false
}, alwaysEqual),
}
return cmp.Equal(x, y, opts)
return opts
}