~samwhited/xmpp

d8d41be58aca80936ded0a753881b3adde96eca2 — Sam Whited 4 years ago e582d95
Start outlining a potential data forms API
4 files changed, 297 insertions(+), 0 deletions(-)

A form/doc.go
A form/fields.go
A form/form.go
A form/options.go
A form/doc.go => form/doc.go +6 -0
@@ 0,0 1,6 @@
// Copyright 2017 Sam Whited.
// Use of this source code is governed by the BSD 2-clause license that can be
// found in the LICENSE file.

// The form package is an implementation of XEP-0004: Data Forms.
package form // import "mellium.im/xmpp/form"

A form/fields.go => form/fields.go +139 -0
@@ 0,0 1,139 @@
// Copyright 2017 Sam Whited.
// Use of this source code is governed by the BSD 2-clause license that can be
// found in the LICENSE file.

package form

import (
	"encoding/xml"
)

// A field represents a data field that may be added to a form.
type field struct {
	XMLName  xml.Name   `xml:"jabber:x:data field"`
	Typ      string     `xml:"type,attr"`
	Var      string     `xml:"var,attr,omitempty"`
	Label    string     `xml:"label,attr,omitempty"`
	Desc     string     `xml:"desc,omitempty"`
	Value    []string   `xml:"value,omitempty"`
	Required struct{}   `xml:"required,omitempty"`
	Option   []fieldopt `xml:"option,omitempty"`
}

type fieldopt struct {
	XMLName xml.Name `xml:"jabber:x:data option"`
	Value   string   `xml:"value,omitempty"`
}

// Boolean fields enable an entity to gather or provide an either-or choice
// between two options.
func Boolean(varName string, o ...FieldOption) Option {
	return func(data *Data) {
		f := field{
			Typ: "boolean",
			Var: varName,
		}
		getFieldOpts(&f, o...)
		data.children = append(data.children, f)
	}
}

// Hidden fields are not shown by the form-submitting entity, but instead are
// returned, generally unmodified, with the form.
func Hidden(varName string, o ...FieldOption) Option {
	return func(data *Data) {
		f := field{
			Typ: "hidden",
			Var: varName,
		}
		getFieldOpts(&f, o...)
		data.children = append(data.children, f)
	}
}

// JIDMulti enables an entity to gather or provide multiple Jabber IDs.
func JIDMulti(varName string, o ...FieldOption) Option {
	return func(data *Data) {
		f := field{
			Typ: "jid-multi",
			Var: varName,
		}
		getFieldOpts(&f, o...)
		data.children = append(data.children, f)
	}
}

// JID enables an entity to gather or provide a Jabber ID.
func JID(varName string, o ...FieldOption) Option {
	return func(data *Data) {
		f := field{
			Typ: "jid-single",
			Var: varName,
		}
		getFieldOpts(&f, o...)
		data.children = append(data.children, f)
	}
}

// ListMulti enables an entity to gather or provide one or more entries from a
// list.
func ListMulti(varName string, o ...FieldOption) Option {
	return func(data *Data) {
		f := field{
			Typ: "list-multi",
			Var: varName,
		}
		getFieldOpts(&f, o...)
		data.children = append(data.children, f)
	}
}

// ListSingle enables an entity to gather or provide a single entry from a list.
func ListSingle(varName string, o ...FieldOption) Option {
	return func(data *Data) {
		f := field{
			Typ: "list-single",
			Var: varName,
		}
		getFieldOpts(&f, o...)
		data.children = append(data.children, f)
	}
}

// TextMulti enables an entity to gather or provide multiple lines of text.
func TextMulti(varName string, o ...FieldOption) Option {
	return func(data *Data) {
		f := field{
			Typ: "text-multi",
			Var: varName,
		}
		getFieldOpts(&f, o...)
		data.children = append(data.children, f)
	}
}

// TextPrivate enables an entity to gather or provide a line of text that should
// be obscured in the submitting entities interface (eg. with multiple
// asterisks).
func TextPrivate(varName string, o ...FieldOption) Option {
	return func(data *Data) {
		f := field{
			Typ: "text-private",
			Var: varName,
		}
		getFieldOpts(&f, o...)
		data.children = append(data.children, f)
	}
}

// TextSingle enables an entity to gather or provide a line of text.
func TextSingle(varName string, o ...FieldOption) Option {
	return func(data *Data) {
		f := field{
			Typ: "text-single",
			Var: varName,
		}
		getFieldOpts(&f, o...)
		data.children = append(data.children, f)
	}
}

A form/form.go => form/form.go +66 -0
@@ 0,0 1,66 @@
// Copyright 2017 Sam Whited.
// Use of this source code is governed by the BSD 2-clause license that can be
// found in the LICENSE file.

package form

import (
	"encoding/xml"
)

var (
	formName = xml.Name{Space: "jabber:x:data", Local: "x"}
)

// Data represents a data form.
type Data struct {
	title struct {
		XMLName xml.Name `xml:"title"`
		Text    string   `xml:",chardata"`
	}
	typ      string
	children []interface{}
}

// MarshalXML satisfies the xml.Marshaler interface for *Data.
func (d *Data) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
	start = xml.StartElement{Name: formName}
	start.Attr = append(start.Attr, xml.Attr{
		Name:  xml.Name{Local: "type"},
		Value: d.typ,
	})
	if err = e.EncodeToken(start); err != nil {
		return
	}

	// Encode the title.
	if d.title.Text != "" {
		if err = e.Encode(d.title); err != nil {
			return
		}
	}

	for _, c := range d.children {
		if err = e.Encode(c); err != nil {
			return
		}
	}

	// Encode the end element of the form.
	if err = e.EncodeToken(start.End()); err != nil {
		return
	}
	return e.Flush()
}

type instructions struct {
	XMLName xml.Name `xml:"instructions"`
	Text    string   `xml:",chardata"`
}

// New builds a new data form from the provided options.
func New(o ...Option) *Data {
	form := &Data{typ: "form"}
	getOpts(form, o...)
	return form
}

A form/options.go => form/options.go +86 -0
@@ 0,0 1,86 @@
// Copyright 2017 Sam Whited.
// Use of this source code is governed by the BSD 2-clause license that can be
// found in the LICENSE file.

package form

// An Option is used to define the behavior and appearance of a data form.
type Option func(*Data)

// Title sets a form's title.
func Title(s string) Option {
	return func(data *Data) {
		data.title.Text = s
	}
}

// Instructions adds new textual instructions to the form.
// Multiple uses of the option result in multiple sets of instructions.
func Instructions(s string) Option {
	return func(data *Data) {
		data.children = append(data.children, instructions{Text: s})
	}
}

func getOpts(data *Data, o ...Option) {
	for _, f := range o {
		f(data)
	}
	return
}

// A FieldOption is used to define the behavior and appearance of a form field.
type FieldOption func(*field)

var (
	// Required flags the field as required in order for the form to be considered
	// valid.
	Required = required
)

var (
	required FieldOption = func(f *field) {
		f.Required = struct{}{}
	}
)

// Desc provides a natural-language description of the field, intended for
// presentation in a user-agent (e.g., as a "tool-tip", help button, or
// explanatory text provided near the field).
// Desc should not contain newlines (the \n and \r characters), since layout is
// the responsibility of a user agent.
// However, it does nothing to prevent them from being added.
func Desc(s string) FieldOption {
	return func(f *field) {
		f.Desc = s
	}
}

// Value defines the default value for the field (according to the
// form-processing entity) in a data form of type "form", the data provided by a
// form-submitting entity in a data form of type "submit", or a data result in a
// data form of type "result".
// Fields of type ListMulti, JidMulti, TextMulti, and Hidden may contain more
// than one Value; all other field types will only use the first Value.
func Value(s string) FieldOption {
	return func(f *field) {
		f.Value = append(f.Value, s)
	}
}

// ListOption is one of the values in a list.
// It has no effect on any non-list field type.
func ListOption(s string) FieldOption {
	return func(f *field) {
		f.Option = append(f.Option, fieldopt{
			Value: s,
		})
	}
}

func getFieldOpts(f *field, o ...FieldOption) {
	for _, opt := range o {
		opt(f)
	}
	return
}