~samwhited/xmpp

65bccd9f2a6a0b2c76e86d4235ed51c0492772c6 — Sam Whited 9 months ago f7c8937
paging: new package implementing RSM

Signed-off-by: Sam Whited <sam@samwhited.com>
M CHANGELOG.md => CHANGELOG.md +2 -0
@@ 6,12 6,14 @@ All notable changes to this project will be documented in this file.

### Added

- paging: new package implementing [XEP-0059: Result Set Management]
- stanza: new functions `AddID` and `AddOriginID` to support unique and stable
  stanza IDs
- version: new package implementing [XEP-0092: Software Version]
- xmpp: new `UnmarshalIQ`, `UnmarshalIQElement`, `IterIQ`, and `IterIQElement`
  methods

[XEP-0059: Result Set Management]: https://xmpp.org/extensions/xep-0059.html
[XEP-0092: Software Version]: https://xmpp.org/extensions/xep-0092.html



M examples/echobot/go.mod => examples/echobot/go.mod +1 -1
@@ 7,7 7,7 @@ require (
	golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect
	golang.org/x/text v0.3.4 // indirect
	mellium.im/sasl v0.2.1
	mellium.im/xmlstream v0.15.2
	mellium.im/xmlstream v0.15.3-0.20210217040345-cc2ffc655204
	mellium.im/xmpp v0.16.0
)


M examples/echobot/go.sum => examples/echobot/go.sum +2 -2
@@ 24,5 24,5 @@ mellium.im/reader v0.1.0 h1:UUEMev16gdvaxxZC7fC08j7IzuDKh310nB6BlwnxTww=
mellium.im/reader v0.1.0/go.mod h1:F+X5HXpkIfJ9EE1zHQG9lM/hO946iYAmU7xjg5dsQHI=
mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w=
mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ=
mellium.im/xmlstream v0.15.2 h1:RleOK10lEsVtzpEZsJeRl4Iu0iC5SQnTQIGJZ7ZHGEc=
mellium.im/xmlstream v0.15.2/go.mod h1:7SUlP7f2qnMczK+Cu/OFgqaIhldMolVjo8np7xG41D0=
mellium.im/xmlstream v0.15.3-0.20210217040345-cc2ffc655204 h1:T/ggGyWYtK8D6BrzyqV6bGRLE1n74ckvQU8laOlPChE=
mellium.im/xmlstream v0.15.3-0.20210217040345-cc2ffc655204/go.mod h1:7SUlP7f2qnMczK+Cu/OFgqaIhldMolVjo8np7xG41D0=

M examples/im/go.sum => examples/im/go.sum +2 -2
@@ 24,5 24,5 @@ mellium.im/reader v0.1.0 h1:UUEMev16gdvaxxZC7fC08j7IzuDKh310nB6BlwnxTww=
mellium.im/reader v0.1.0/go.mod h1:F+X5HXpkIfJ9EE1zHQG9lM/hO946iYAmU7xjg5dsQHI=
mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w=
mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ=
mellium.im/xmlstream v0.15.2 h1:RleOK10lEsVtzpEZsJeRl4Iu0iC5SQnTQIGJZ7ZHGEc=
mellium.im/xmlstream v0.15.2/go.mod h1:7SUlP7f2qnMczK+Cu/OFgqaIhldMolVjo8np7xG41D0=
mellium.im/xmlstream v0.15.3-0.20210217040345-cc2ffc655204 h1:T/ggGyWYtK8D6BrzyqV6bGRLE1n74ckvQU8laOlPChE=
mellium.im/xmlstream v0.15.3-0.20210217040345-cc2ffc655204/go.mod h1:7SUlP7f2qnMczK+Cu/OFgqaIhldMolVjo8np7xG41D0=

M examples/msgrepl/go.mod => examples/msgrepl/go.mod +1 -1
@@ 7,7 7,7 @@ require (
	golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect
	golang.org/x/text v0.3.4 // indirect
	mellium.im/sasl v0.2.1
	mellium.im/xmlstream v0.15.2
	mellium.im/xmlstream v0.15.3-0.20210217040345-cc2ffc655204
	mellium.im/xmpp v0.16.0
)


M examples/msgrepl/go.sum => examples/msgrepl/go.sum +2 -2
@@ 24,5 24,5 @@ mellium.im/reader v0.1.0 h1:UUEMev16gdvaxxZC7fC08j7IzuDKh310nB6BlwnxTww=
mellium.im/reader v0.1.0/go.mod h1:F+X5HXpkIfJ9EE1zHQG9lM/hO946iYAmU7xjg5dsQHI=
mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w=
mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ=
mellium.im/xmlstream v0.15.2 h1:RleOK10lEsVtzpEZsJeRl4Iu0iC5SQnTQIGJZ7ZHGEc=
mellium.im/xmlstream v0.15.2/go.mod h1:7SUlP7f2qnMczK+Cu/OFgqaIhldMolVjo8np7xG41D0=
mellium.im/xmlstream v0.15.3-0.20210217040345-cc2ffc655204 h1:T/ggGyWYtK8D6BrzyqV6bGRLE1n74ckvQU8laOlPChE=
mellium.im/xmlstream v0.15.3-0.20210217040345-cc2ffc655204/go.mod h1:7SUlP7f2qnMczK+Cu/OFgqaIhldMolVjo8np7xG41D0=

M go.mod => go.mod +1 -1
@@ 9,5 9,5 @@ require (
	golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061
	golang.org/x/text v0.3.2
	mellium.im/sasl v0.2.1
	mellium.im/xmlstream v0.15.2
	mellium.im/xmlstream v0.15.3-0.20210217040345-cc2ffc655204
)

M go.sum => go.sum +2 -2
@@ 19,5 19,5 @@ mellium.im/reader v0.1.0 h1:UUEMev16gdvaxxZC7fC08j7IzuDKh310nB6BlwnxTww=
mellium.im/reader v0.1.0/go.mod h1:F+X5HXpkIfJ9EE1zHQG9lM/hO946iYAmU7xjg5dsQHI=
mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w=
mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ=
mellium.im/xmlstream v0.15.2 h1:RleOK10lEsVtzpEZsJeRl4Iu0iC5SQnTQIGJZ7ZHGEc=
mellium.im/xmlstream v0.15.2/go.mod h1:7SUlP7f2qnMczK+Cu/OFgqaIhldMolVjo8np7xG41D0=
mellium.im/xmlstream v0.15.3-0.20210217040345-cc2ffc655204 h1:T/ggGyWYtK8D6BrzyqV6bGRLE1n74ckvQU8laOlPChE=
mellium.im/xmlstream v0.15.3-0.20210217040345-cc2ffc655204/go.mod h1:7SUlP7f2qnMczK+Cu/OFgqaIhldMolVjo8np7xG41D0=

A paging/rsm.go => paging/rsm.go +124 -0
@@ 0,0 1,124 @@
// 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 paging implements result set management.
package paging // import "mellium.im/xmpp/paging"

import (
	"encoding/xml"

	"mellium.im/xmlstream"
)

// Namespaces used by this package.
const (
	NS = "http://jabber.org/protocol/rsm"
)

// Iter provides a mechanism for iterating over the children of an XML element.
// Successive calls to Next will step through each child, returning its start
// element and a reader that is limited to the remainder of the child.
//
// If the results indicate that there is another page of data, the paging child
// is skipped and the various paging methods will return queries that can be
// used to fetch the next and/or previous pages.
type Iter struct {
	iter        *xmlstream.Iter
	nextPageSet *RequestNext
	prevPageSet *RequestPrev
	curSet      *Set
	err         error
	max         uint64
}

// NewIter returns a new iterator that iterates over the children of the most
// recent start element already consumed from r.
func NewIter(r xml.TokenReader, max uint64) *Iter {
	return &Iter{
		iter: xmlstream.NewIter(r),
		max:  max,
	}
}

// Close indicates that we are finished with the given iterator. Calling it
// multiple times has no effect.
//
// If the underlying TokenReader is also an io.Closer, Close calls the readers
// Close method.
func (i *Iter) Close() error {
	return i.iter.Close()
}

// Current returns a reader over the most recent child.
func (i *Iter) Current() (*xml.StartElement, xml.TokenReader) {
	return i.iter.Current()
}

// Err returns the last error encountered by the iterator (if any).
func (i *Iter) Err() error {
	if i.err != nil {
		return i.err
	}
	return i.iter.Err()
}

// Next returns true if there are more items to decode.
func (i *Iter) Next() bool {
	if i.err != nil {
		return false
	}
	hasNext := i.iter.Next()
	if hasNext {
		start, r := i.iter.Current()
		if start != nil && start.Name.Local == "set" && start.Name.Space == NS {
			i.nextPageSet = nil
			i.prevPageSet = nil
			i.curSet = &Set{}
			i.err = xml.NewTokenDecoder(xmlstream.MultiReader(xmlstream.Token(*start), r)).Decode(i.curSet)
			if i.err != nil {
				return false
			}
			if i.curSet.First.ID != "" {
				i.prevPageSet = &RequestPrev{
					Before: i.curSet.First.ID,
					Max:    i.max,
				}
			}
			if i.curSet.Last != "" {
				i.nextPageSet = &RequestNext{
					After: i.curSet.Last,
					Max:   i.max,
				}
			}
			return i.Next()
		}
	}
	return hasNext
}

// NextPage returns a value that can be used to construct a new iterator that
// queries for the next page.
//
// It is only guaranteed to be set once iteration is finished, or when the
// iterator is closed without error and may be nil.
func (i *Iter) NextPage() *RequestNext {
	return i.nextPageSet
}

// PreviousPage returns a value that can be used to construct a new iterator that
// queries for the previous page.
//
// It is only guaranteed to be set once iteration is finished, or when the
// iterator is closed without error and may be nil.
func (i *Iter) PreviousPage() *RequestPrev {
	return i.prevPageSet
}

// CurrentPage returns information about the current page.
//
// It is only guaranteed to be set once iteration is finished, or when the
// iterator is closed without error and may be nil.
func (i *Iter) CurrentPage() *Set {
	return i.curSet
}

A paging/rsm_test.go => paging/rsm_test.go +144 -0
@@ 0,0 1,144 @@
// 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 paging_test

import (
	"encoding/xml"
	"strconv"
	"strings"
	"testing"

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

var (
	_ xmlstream.Marshaler = (*paging.RequestCount)(nil)
	_ xmlstream.WriterTo  = (*paging.RequestCount)(nil)
	_ xml.Marshaler       = (*paging.RequestCount)(nil)
	_ xmlstream.Marshaler = (*paging.RequestNext)(nil)
	_ xmlstream.WriterTo  = (*paging.RequestNext)(nil)
	_ xml.Marshaler       = (*paging.RequestNext)(nil)
	_ xmlstream.Marshaler = (*paging.RequestPrev)(nil)
	_ xmlstream.WriterTo  = (*paging.RequestPrev)(nil)
	_ xml.Marshaler       = (*paging.RequestPrev)(nil)
	_ xmlstream.Marshaler = (*paging.RequestIndex)(nil)
	_ xmlstream.WriterTo  = (*paging.RequestIndex)(nil)
	_ xml.Marshaler       = (*paging.RequestIndex)(nil)
	_ xmlstream.Marshaler = (*paging.Set)(nil)
	_ xmlstream.WriterTo  = (*paging.Set)(nil)
	_ xml.Marshaler       = (*paging.Set)(nil)
)

var iterTests = [...]struct {
	in          string
	out         string
	nextQueries string
	prevQueries string
	curQueries  string
	err         error
}{
	0: {
		in: `<a></a>`,
	},
	1: {
		in:  `<nums><a>1</a><a/></nums>`,
		out: `<a>1</a><a></a>`,
	},
	2: {
		in: `<nums><a>1</a><b/><set xmlns='http://jabber.org/protocol/rsm'>
<last>2</last>
</set>
</nums>`,
		out:         "<a>1</a><b></b>\n",
		nextQueries: `<set xmlns="http://jabber.org/protocol/rsm"><max>10</max><after>2</after></set>`,
		curQueries:  `<set xmlns="http://jabber.org/protocol/rsm"><first></first><last>2</last></set>`,
	},
	3: {
		in: `<nums><set xmlns='http://jabber.org/protocol/rsm'>
<first>1</first>
</set><b/></nums>`,
		out:         "<b></b>",
		prevQueries: `<set xmlns="http://jabber.org/protocol/rsm"><before>1</before><max>10</max></set>`,
		curQueries:  `<set xmlns="http://jabber.org/protocol/rsm"><first>1</first><last></last></set>`,
	},
}

func TestIter(t *testing.T) {
	for i, tc := range iterTests {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			var buf, curQueries, nextQueries, prevQueries strings.Builder
			d := xml.NewDecoder(strings.NewReader(tc.in))
			e := xml.NewEncoder(&buf)
			_, err := d.Token()
			if err != nil {
				t.Fatalf("error popping first token: %v", err)
			}
			iter := paging.NewIter(d, 10)
			nextSet := iter.NextPage()
			if nextSet != nil {
				t.Fatalf("should not start with next page set, got %+v", nextSet)
			}
			for iter.Next() {
				start, r := iter.Current()
				if start != nil {
					err := e.EncodeToken(*start)
					if err != nil {
						t.Fatalf("error encoding start element: %v", err)
					}
				}
				_, err = xmlstream.Copy(e, r)
				if err != nil {
					t.Fatalf("error encoding stream: %v", err)
				}
			}
			if err := iter.Err(); err != nil {
				t.Fatalf("error iterating: %v", err)
			}
			if err := e.Flush(); err != nil {
				t.Fatalf("error flushing output: %v", err)
			}
			// Next
			query, err := xml.Marshal(iter.NextPage())
			if err != nil {
				t.Fatalf("error marshaling next set: %v", err)
			}
			_, err = nextQueries.Write(query)
			if err != nil {
				t.Fatalf("error writing next query: %v", err)
			}
			// Prev
			query, err = xml.Marshal(iter.PreviousPage())
			if err != nil {
				t.Fatalf("error marshaling previous set: %v", err)
			}
			_, err = prevQueries.Write(query)
			if err != nil {
				t.Fatalf("error writing prev query: %v", err)
			}
			// Current
			query, err = xml.Marshal(iter.CurrentPage())
			if err != nil {
				t.Fatalf("error marshaling current set: %v", err)
			}
			_, err = curQueries.Write(query)
			if err != nil {
				t.Fatalf("error writing current query: %v", err)
			}
			if out := buf.String(); out != tc.out {
				t.Errorf("wrong output: want=%s, got=%s", tc.out, out)
			}
			if q := nextQueries.String(); q != tc.nextQueries {
				t.Errorf("wrong next queries:\nwant=%s,\n got=%s", tc.nextQueries, q)
			}
			if q := prevQueries.String(); q != tc.prevQueries {
				t.Errorf("wrong prev queries:\nwant=%s,\n got=%s", tc.prevQueries, q)
			}
			if q := curQueries.String(); q != tc.curQueries {
				t.Errorf("wrong current queries:\nwant=%s,\n got=%s", tc.curQueries, q)
			}
		})
	}
}

A paging/types.go => paging/types.go +203 -0
@@ 0,0 1,203 @@
// 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 paging

import (
	"encoding/xml"
	"strconv"

	"mellium.im/xmlstream"
)

// RequestCount can be added to a query to request the count of elements without
// returning any actual items.
type RequestCount struct {
	XMLName xml.Name `xml:"http://jabber.org/protocol/rsm set"`
}

// TokenReader implements xmlstream.Marshaler.
func (req *RequestCount) TokenReader() xml.TokenReader {
	return xmlstream.Wrap(
		xmlstream.Wrap(
			xmlstream.Token(xml.CharData("0")),
			xml.StartElement{Name: xml.Name{Local: "max"}},
		),
		xml.StartElement{Name: xml.Name{Space: NS, Local: "set"}},
	)
}

// WriteXML implements xmlstream.WriterTo.
func (req *RequestCount) WriteXML(w xmlstream.TokenWriter) (int, error) {
	return xmlstream.Copy(w, req.TokenReader())
}

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

// RequestNext can be added to a query to request the first page or to page
// forward.
type RequestNext struct {
	XMLName xml.Name `xml:"http://jabber.org/protocol/rsm set"`
	Max     uint64   `xml:"max,omitempty"`
	After   string   `xml:"after,omitempty"`
}

// TokenReader implements xmlstream.Marshaler.
func (req *RequestNext) TokenReader() xml.TokenReader {
	var payloads []xml.TokenReader
	if req.Max > 0 {
		payloads = append(payloads, xmlstream.Wrap(
			xmlstream.Token(xml.CharData(strconv.FormatUint(req.Max, 10))),
			xml.StartElement{Name: xml.Name{Local: "max"}},
		))
	}
	if req.After != "" {
		payloads = append(payloads, xmlstream.Wrap(
			xmlstream.Token(xml.CharData(req.After)),
			xml.StartElement{Name: xml.Name{Local: "after"}},
		))
	}
	return xmlstream.Wrap(
		xmlstream.MultiReader(payloads...),
		xml.StartElement{Name: xml.Name{Space: NS, Local: "set"}},
	)
}

// WriteXML implements xmlstream.WriterTo.
func (req *RequestNext) WriteXML(w xmlstream.TokenWriter) (int, error) {
	return xmlstream.Copy(w, req.TokenReader())
}

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

// RequestPrev can be added to a query to request the last page or to page
// backward.
type RequestPrev struct {
	XMLName xml.Name `xml:"http://jabber.org/protocol/rsm set"`
	Max     uint64   `xml:"max,omitempty"`
	Before  string   `xml:"before"`
}

// TokenReader implements xmlstream.Marshaler.
func (req *RequestPrev) TokenReader() xml.TokenReader {
	payloads := []xml.TokenReader{xmlstream.Wrap(
		xmlstream.Token(xml.CharData(req.Before)),
		xml.StartElement{Name: xml.Name{Local: "before"}},
	)}
	if req.Max > 0 {
		payloads = append(payloads, xmlstream.Wrap(
			xmlstream.Token(xml.CharData(strconv.FormatUint(req.Max, 10))),
			xml.StartElement{Name: xml.Name{Local: "max"}},
		))
	}
	return xmlstream.Wrap(
		xmlstream.MultiReader(payloads...),
		xml.StartElement{Name: xml.Name{Space: NS, Local: "set"}},
	)
}

// WriteXML implements xmlstream.WriterTo.
func (req *RequestPrev) WriteXML(w xmlstream.TokenWriter) (int, error) {
	return xmlstream.Copy(w, req.TokenReader())
}

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

// RequestIndex can be added to a query to skip to a specific page.
// It is not always supported.
type RequestIndex struct {
	XMLName xml.Name `xml:"http://jabber.org/protocol/rsm set"`
	Max     uint64   `xml:"max"`
	Index   uint64   `xml:"index"`
}

// TokenReader implements xmlstream.Marshaler.
func (req *RequestIndex) TokenReader() xml.TokenReader {
	payloads := []xml.TokenReader{xmlstream.Wrap(
		xmlstream.Token(xml.CharData(strconv.FormatUint(req.Index, 10))),
		xml.StartElement{Name: xml.Name{Local: "index"}},
	), xmlstream.Wrap(
		xmlstream.Token(xml.CharData(strconv.FormatUint(req.Max, 10))),
		xml.StartElement{Name: xml.Name{Local: "max"}},
	)}
	return xmlstream.Wrap(
		xmlstream.MultiReader(payloads...),
		xml.StartElement{Name: xml.Name{Space: NS, Local: "set"}},
	)
}

// WriteXML implements xmlstream.WriterTo.
func (req *RequestIndex) WriteXML(w xmlstream.TokenWriter) (int, error) {
	return xmlstream.Copy(w, req.TokenReader())
}

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

// Set describes a page from a returned result set.
type Set struct {
	XMLName xml.Name `xml:"http://jabber.org/protocol/rsm set"`
	First   struct {
		ID    string  `xml:",cdata"`
		Index *uint64 `xml:"index,omitempty"`
	} `xml:"first"`
	Last  string  `xml:"last"`
	Count *uint64 `xml:"count,omitempty"`
}

// TokenReader implements xmlstream.Marshaler.
func (s *Set) TokenReader() xml.TokenReader {
	var payloads []xml.TokenReader
	start := xml.StartElement{Name: xml.Name{Local: "first"}}
	if s.First.Index != nil {
		start.Attr = append(start.Attr, xml.Attr{
			Name:  xml.Name{Local: "index"},
			Value: strconv.FormatUint(*s.First.Index, 10),
		})
	}
	payloads = append(payloads, xmlstream.Wrap(
		xmlstream.Token(xml.CharData(s.First.ID)),
		start,
	))
	payloads = append(payloads, xmlstream.Wrap(
		xmlstream.Token(xml.CharData(s.Last)),
		xml.StartElement{Name: xml.Name{Local: "last"}},
	))
	if s.Count != nil {
		payloads = append(payloads, xmlstream.Wrap(
			xmlstream.Token(xml.CharData(strconv.FormatUint(*s.Count, 10))),
			xml.StartElement{Name: xml.Name{Local: "count"}},
		))
	}
	return xmlstream.Wrap(
		xmlstream.MultiReader(payloads...),
		xml.StartElement{Name: xml.Name{Space: NS, Local: "set"}},
	)
}

// WriteXML implements xmlstream.WriterTo.
func (s *Set) WriteXML(w xmlstream.TokenWriter) (int, error) {
	return xmlstream.Copy(w, s.TokenReader())
}

// MarshalXML satisfies the xml.Marshaler interface.
func (s *Set) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	_, err := s.WriteXML(e)
	return err
}