diff --git a/_examples/delegation/delegation.go b/_examples/delegation/delegation.go index 9e26b69..31c18d3 100644 --- a/_examples/delegation/delegation.go +++ b/_examples/delegation/delegation.go @@ -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"}, diff --git a/stanza/README.md b/stanza/README.md index fa93acf..466f594 100644 --- a/stanza/README.md +++ b/stanza/README.md @@ -8,10 +8,11 @@ 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 -struct that you can build by hand. +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 diff --git a/stanza/builder.go b/stanza/builder.go deleted file mode 100644 index 23d131f..0000000 --- a/stanza/builder.go +++ /dev/null @@ -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{} -} diff --git a/stanza/iq_disco.go b/stanza/iq_disco.go index da161d1..0097c05 100644 --- a/stanza/iq_disco.go +++ b/stanza/iq_disco.go @@ -1,27 +1,80 @@ 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" + NSDiscoInfo = "http://jabber.org/protocol/disco#info" ) -// 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"` - Features []Feature `xml:"feature"` + XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"` + Node string `xml:"node,attr,omitempty"` + Identity []Identity `xml:"identity"` + Features []Feature `xml:"feature"` } 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"` diff --git a/stanza/iq_disco_test.go b/stanza/iq_disco_test.go new file mode 100644 index 0000000..8b76767 --- /dev/null +++ b/stanza/iq_disco_test.go @@ -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) + } + } +} diff --git a/stanza/iq_test.go b/stanza/iq_test.go index 39df2aa..04a868a 100644 --- a/stanza/iq_test.go +++ b/stanza/iq_test.go @@ -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", - Category: "gateway", - Type: "mqtt", - }, + 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)) } } diff --git a/stanza/xmpp_test.go b/stanza/xmpp_test.go index 2b24526..611948a 100644 --- a/stanza/xmpp_test.go +++ b/stanza/xmpp_test.go @@ -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 }