~samwhited/xmpp

5dc440169bf44b847193eea58a1ebc030c0f4484 — Sam Whited 3 months ago 79ab5e1
disco: move Item to new items package.

Previously we had planned to move Item into the info package so that
iterators for items and the like could live alongside iterators for
info. However, Items require using JID which would result in yet another
import loop. At the risk of creating tiny package syndrome, we've
instead opted to create an items package as well since the JID package
will likely never use the items side of service discovery.

Signed-off-by: Sam Whited <sam@samwhited.com>
5 files changed, 115 insertions(+), 47 deletions(-)

M CHANGELOG.md
M disco/items.go
A disco/items/items.go
A disco/items/items_test.go
M disco/items_test.go
M CHANGELOG.md => CHANGELOG.md +2 -1
@@ 10,7 10,8 @@ All notable changes to this project will be documented in this file.

### Breaking

- disco: the `Feature` type moved to `info.Feature`
- disco: the `Feature` and `Item` types have been moved to the `info` and
  `items` packages
- roster: rename `version` attribute to `ver`
- roster: the `Push` callback now takes the roster version
- roster: `FetchIQ` now takes a `roster.IQ` instead of a `stanza.IQ` so that the

M disco/items.go => disco/items.go +10 -41
@@ 11,7 11,7 @@ import (

	"mellium.im/xmlstream"
	"mellium.im/xmpp"
	"mellium.im/xmpp/jid"
	"mellium.im/xmpp/disco/items"
	"mellium.im/xmpp/paging"
	"mellium.im/xmpp/stanza"
)


@@ 40,42 40,11 @@ func (q ItemsQuery) WriteXML(w xmlstream.TokenWriter) (int, error) {
	return xmlstream.Copy(w, q.TokenReader())
}

// Item represents a discovered item.
type Item struct {
	XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items item"`
	JID     jid.JID  `xml:"jid,attr"`
	Name    string   `xml:"name,attr,omitempty"`
	Node    string   `xml:"node,attr,omitempty"`
}

// TokenReader implements xmlstream.Marshaler.
func (i Item) TokenReader() xml.TokenReader {
	start := xml.StartElement{
		Name: xml.Name{Space: NSItems, Local: "item"},
		Attr: []xml.Attr{{
			Name:  xml.Name{Local: "jid"},
			Value: i.JID.String(),
		}},
	}
	if i.Node != "" {
		start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "node"}, Value: i.Node})
	}
	if i.Name != "" {
		start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "name"}, Value: i.Name})
	}
	return xmlstream.Wrap(nil, start)
}

// WriteXML implements xmlstream.WriterTo.
func (i Item) WriteXML(w xmlstream.TokenWriter) (int, error) {
	return xmlstream.Copy(w, i.TokenReader())
}

// ItemIter is an iterator over discovered items.
// It supports paging
type ItemIter struct {
	iter    *paging.Iter
	current Item
	current items.Item
	err     error
	ctx     context.Context
	session *xmpp.Session


@@ 102,7 71,7 @@ func (i *ItemIter) Next() bool {
			return i.Next()
		}
		d := xml.NewTokenDecoder(xmlstream.MultiReader(xmlstream.Token(*start), r))
		item := Item{}
		item := items.Item{}
		i.err = d.Decode(&item)
		if i.err != nil {
			return false


@@ 131,7 100,7 @@ func (i *ItemIter) Err() error {
}

// Item returns the last item parsed by the iterator.
func (i *ItemIter) Item() Item {
func (i *ItemIter) Item() items.Item {
	return i.current
}



@@ 154,7 123,7 @@ func (i *ItemIter) Close() error {
// The iterator must be closed before anything else is done on the session.
// Any errors encountered while creating the iter are deferred until the iter is
// used.
func FetchItems(ctx context.Context, item Item, s *xmpp.Session) *ItemIter {
func FetchItems(ctx context.Context, item items.Item, s *xmpp.Session) *ItemIter {
	return FetchItemsIQ(ctx, item.Node, stanza.IQ{To: item.JID}, s)
}



@@ 199,7 168,7 @@ var ErrSkipItem = errors.New("skip this item")
// bypass the query entirely.
// If an error occurs while making the query, the function will be called again
// with the same item to report the error.
type WalkItemFunc func(level int, item Item, err error) error
type WalkItemFunc func(level int, item items.Item, err error) error

// WalkItem walks the tree rooted at the JID, calling fn for each item in the
// tree, including root.


@@ 211,15 180,15 @@ type WalkItemFunc func(level int, item Item, err error) error
//
// The items are walked in wire order which may make the output
// non-deterministic.
func WalkItem(ctx context.Context, item Item, s *xmpp.Session, fn WalkItemFunc) error {
	return walkItem(ctx, 0, 0, []Item{item}, s, fn)
func WalkItem(ctx context.Context, item items.Item, s *xmpp.Session, fn WalkItemFunc) error {
	return walkItem(ctx, 0, 0, []items.Item{item}, s, fn)
}

func ignoredErr(err error) bool {
	return errors.Is(err, stanza.Error{Condition: stanza.FeatureNotImplemented}) || errors.Is(err, stanza.Error{Condition: stanza.ServiceUnavailable})
}

func walkItem(ctx context.Context, level, itemIdx int, items []Item, s *xmpp.Session, fn WalkItemFunc) error {
func walkItem(ctx context.Context, level, itemIdx int, items []items.Item, s *xmpp.Session, fn WalkItemFunc) error {
	last := len(items) - 1
	item := items[itemIdx]
	err := fn(level, item, nil)


@@ 264,7 233,7 @@ func walkItem(ctx context.Context, level, itemIdx int, items []Item, s *xmpp.Ses
	return nil
}

func appendItems(ctx context.Context, s *xmpp.Session, itemIdx int, items []Item) (i []Item, err error) {
func appendItems(ctx context.Context, s *xmpp.Session, itemIdx int, items []items.Item) (i []items.Item, err error) {
	iter := FetchItems(ctx, items[itemIdx], s)
	defer func() {
		e := iter.Close()

A disco/items/items.go => disco/items/items.go +56 -0
@@ 0,0 1,56 @@
// Copyright 2021 The Mellium Contributors.
// Use of this source code is governed by the BSD 2-clause
// license that can be found in the LICENSE file.

// Package items contains service discovery items.
//
// These were separated out into a separate package to prevent import loops.
package items // import "mellium.im/xmpp/disco/items"

import (
	"encoding/xml"

	"mellium.im/xmlstream"
	"mellium.im/xmpp/jid"
)

const (
	ns = `http://jabber.org/protocol/disco#items`
)

// Item represents a discovered item.
type Item struct {
	XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items item"`
	JID     jid.JID  `xml:"jid,attr"`
	Name    string   `xml:"name,attr,omitempty"`
	Node    string   `xml:"node,attr,omitempty"`
}

// TokenReader implements xmlstream.Marshaler.
func (i Item) TokenReader() xml.TokenReader {
	start := xml.StartElement{
		Name: xml.Name{Space: ns, Local: "item"},
		Attr: []xml.Attr{{
			Name:  xml.Name{Local: "jid"},
			Value: i.JID.String(),
		}},
	}
	if i.Node != "" {
		start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "node"}, Value: i.Node})
	}
	if i.Name != "" {
		start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "name"}, Value: i.Name})
	}
	return xmlstream.Wrap(nil, start)
}

// WriteXML implements xmlstream.WriterTo.
func (i Item) WriteXML(w xmlstream.TokenWriter) (int, error) {
	return xmlstream.Copy(w, i.TokenReader())
}

// MarshalXML implements xml.Marshaler.
func (i Item) MarshalXML(e *xml.Encoder, _ xml.StartElement) error {
	_, err := i.WriteXML(e)
	return err
}

A disco/items/items_test.go => disco/items/items_test.go +41 -0
@@ 0,0 1,41 @@
// Copyright 2021 The Mellium Contributors.
// Use of this source code is governed by the BSD 2-clause
// license that can be found in the LICENSE file.

package items_test

import (
	"encoding/xml"
	"testing"

	"mellium.im/xmlstream"
	"mellium.im/xmpp/disco"
	"mellium.im/xmpp/disco/items"
	"mellium.im/xmpp/internal/xmpptest"
	"mellium.im/xmpp/jid"
)

var (
	_ xml.Marshaler       = items.Item{}
	_ xmlstream.Marshaler = items.Item{}
	_ xmlstream.WriterTo  = items.Item{}
)

func TestEncode(t *testing.T) {
	xmpptest.RunEncodingTests(t, []xmpptest.EncodingTestCase{
		0: {
			Value:       &items.Item{},
			XML:         `<item xmlns="http://jabber.org/protocol/disco#items" jid=""></item>`,
			NoUnmarshal: true,
		},
		1: {
			Value: &items.Item{
				XMLName: xml.Name{Space: disco.NSItems, Local: "item"},
				JID:     jid.MustParse("example.net"),
				Node:    "urn:example",
				Name:    "test",
			},
			XML: `<item xmlns="http://jabber.org/protocol/disco#items" jid="example.net" node="urn:example" name="test"></item>`,
		},
	})
}

M disco/items_test.go => disco/items_test.go +6 -5
@@ 14,6 14,7 @@ import (

	"mellium.im/xmlstream"
	"mellium.im/xmpp/disco"
	"mellium.im/xmpp/disco/items"
	"mellium.im/xmpp/internal/xmpptest"
	"mellium.im/xmpp/jid"
	"mellium.im/xmpp/stanza"


@@ 21,12 22,12 @@ import (

var testFetchItems = [...]struct {
	node  string
	items map[string][]disco.Item
	items map[string][]items.Item
	err   error
}{
	0: {},
	1: {
		items: map[string][]disco.Item{
		items: map[string][]items.Item{
			"": {{
				XMLName: xml.Name{Space: disco.NSItems, Local: "item"},
				JID:     jid.MustParse("juliet@example.com"),


@@ 39,7 40,7 @@ var testFetchItems = [...]struct {
	},
	2: {
		node: "test",
		items: map[string][]disco.Item{
		items: map[string][]items.Item{
			"test": {{
				XMLName: xml.Name{Space: disco.NSItems, Local: "item"},
				JID:     jid.MustParse("benvolio@example.org"),


@@ 50,7 51,7 @@ var testFetchItems = [...]struct {

type queryItems struct {
	XMLName xml.Name     `xml:"http://jabber.org/protocol/disco#items query"`
	Items   []disco.Item `xml:"item"`
	Items   []items.Item `xml:"item"`
}

func TestFetchItems(t *testing.T) {


@@ 80,7 81,7 @@ func TestFetchItems(t *testing.T) {
				}),
			)
			iter := disco.FetchItemsIQ(context.Background(), tc.node, IQ, cs.Client)
			items := make([]disco.Item, 0, len(tc.items))
			items := make([]items.Item, 0, len(tc.items))
			for iter.Next() {
				items = append(items, iter.Item())
			}