hmc

package module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 21, 2026 License: CC0-1.0 Imports: 10 Imported by: 0

README

Hyper Media Controls

hmc's primary purpose is to provide human-readable XML wrappers to a REST interface, to guide the user in how to interact with the interface. Similar to HTML, but stripped down for readability; generally on the commandline using a tool like HTTPie.

An XML element under the c: namespace is a hypermedia control provided by this package that tells the user what interactions on the given resource are possible.

There are five main elements that hmc provides. They generally try to mimic the existing standard of HTML:

  • <c:Form>: analogous to HTML's <form>. It encloses a group of inputs, and generally describes which HTTP verb to use under the method attribute, e.g. POST or by default GET.
  • <c:Input>: analogous to HTML's <input> type. It represents a single name-value pair. It may have validation attributes, similar to HTML: e.g. type, required, minlength. An <c:Input> (or a <c:Select>, or a <c:Map>) outside of a <c:Form> is not a valid input.
  • <c:Select>: analogous to HTML's <select>. It represents an input with fixed options. The option list may be non-exaustive. It may take multiple options if the multiple attribute is set.
  • <c:Link>: analogous to HTML's <a> hyperlink. It provides directions to other relevant resources. A <c:Link> inside of a <c:Form> should be considered to represent an alternate form action.
  • <c:Map>: Is the only element without an HTML analogue. A <c:Map> with name="foo" means that arbitrary name-value pairs may be provided under the namespace "foo" with bracket notation, e.g. foo[bar]=baz.

These elements can also be serialised to JSON for ease of querying, especially using jq.

Documentation

Overview

Package hmc provides types for building HATEOAS-driven REST APIs with progressive enhancement.

Each type represents a hypermedia control—a semantic element that guides client state transitions. These controls can be serialized as JSON, XML, or wrapped in HTML templates depending on the client's capabilities.

Philosophy

Traditional REST APIs serve data (JSON) and leave state transitions to out-of-band documentation. HATEOAS APIs serve hypermedia—data enriched with the controls needed to interact with it. This library provides the building blocks for those controls.

The types in hmc are deliberately minimal, focusing on the semantic layer rather than presentation. They describe WHAT actions are available and HOW to invoke them, not how they should be styled or rendered.

Progressive Enhancement

The same endpoint can serve multiple representations:

  • JSON: Machine-readable, suitable for scripts and traditional API clients
  • XML: Human-readable structure for CLI tools (curl, HTTPie + xmllint)
  • HTML: Browser-ready interfaces when wrapped with your templates

This enables a "write once, serve many" approach where a single handler can satisfy browsers, CLIs, and programmatic clients through content negotiation.

Usage

Types are designed to be embedded in your domain structs:

type LoginPage struct {
    LoginForm hmc.Form[struct {
        Username hmc.Input
        Password hmc.Input
    }]
    RegisterLink hmc.Link
}

Marshal to JSON for API clients, XML for CLI tools, or wrap in HTML templates for browsers. Examples at ./examples/templates.

Input validation is minimal and extensible—Validate() checks Required and MinLength, matching basic browser behavior. Extend by inspecting Input.Value and setting Input.Error for domain-specific rules.

What This Is Not

This is not a complete HTML generation framework. You bring your own templates for presentation (you should use the examples as a starting point). This is not a validation framework—it provides minimal checks that may be extended by you.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ErrInputMax added in v0.6.0

type ErrInputMax struct {
	Max string
}

func (ErrInputMax) Error added in v0.6.0

func (e ErrInputMax) Error() string

type ErrInputMaxLength added in v0.6.0

type ErrInputMaxLength struct {
	MaxLength uint
}

func (ErrInputMaxLength) Error added in v0.6.0

func (e ErrInputMaxLength) Error() string

type ErrInputMin added in v0.6.0

type ErrInputMin struct {
	Min string
}

func (ErrInputMin) Error added in v0.6.0

func (e ErrInputMin) Error() string

type ErrInputMinLength added in v0.6.0

type ErrInputMinLength struct {
	MinLength uint
}

func (ErrInputMinLength) Error added in v0.6.0

func (e ErrInputMinLength) Error() string

type ErrInputRequired added in v0.6.0

type ErrInputRequired struct{}

func (ErrInputRequired) Error added in v0.6.0

func (e ErrInputRequired) Error() string

type ErrInputValueAsDate added in v0.6.0

type ErrInputValueAsDate struct {
	Err error
}

func (ErrInputValueAsDate) Error added in v0.6.0

func (e ErrInputValueAsDate) Error() string

type ErrInputValueAsDatetime added in v0.6.0

type ErrInputValueAsDatetime struct {
	Err error
}

func (ErrInputValueAsDatetime) Error added in v0.6.0

func (e ErrInputValueAsDatetime) Error() string

type ErrInputValueAsDatetimeLocal added in v0.6.0

type ErrInputValueAsDatetimeLocal struct {
	Err error
}

func (ErrInputValueAsDatetimeLocal) Error added in v0.6.0

type ErrInputValueAsTime added in v0.6.0

type ErrInputValueAsTime struct {
	Err error
}

func (ErrInputValueAsTime) Error added in v0.6.0

func (e ErrInputValueAsTime) Error() string

type ErrMapMaxEntries added in v0.6.0

type ErrMapMaxEntries struct {
	Max int
}

func (ErrMapMaxEntries) Error added in v0.6.0

func (e ErrMapMaxEntries) Error() string

type ErrMapMaxLength added in v0.6.0

type ErrMapMaxLength struct {
	Key       string
	MaxLength int
}

func (ErrMapMaxLength) Error added in v0.6.0

func (e ErrMapMaxLength) Error() string

type ErrMapMaxValues added in v0.6.0

type ErrMapMaxValues struct {
	Key       string
	MaxLength int
}

func (ErrMapMaxValues) Error added in v0.6.0

func (e ErrMapMaxValues) Error() string

type ErrSelectHasNonOption added in v0.6.0

type ErrSelectHasNonOption struct{}

func (ErrSelectHasNonOption) Error added in v0.6.0

func (ErrSelectHasNonOption) Error() string

type Form

type Form[T any] struct {
	Method   string `json:"method,omitempty"`
	Action   string `json:"action,omitempty"`
	Elements T      `json:"elements"`
}

Form is analogous to HTML's <form> which represents a state transition that requires input from the client. It describes what data is needed, how it should be submitted, and where it should be sent.

Elements should contain a struct representing, and elements semantically related to the form, which would usually be Input, Select, Map, Link, etc. but might also be something like `Error string` or `Warning string` fields.

func (Form[T]) MarshalXML

func (i Form[T]) MarshalXML(e *xml.Encoder, start xml.StartElement) error

type Input

type Input struct {
	Label     string
	Type      string
	Name      string
	Value     string
	Error     string
	Required  bool
	Disabled  bool
	MinLength uint
	MaxLength uint
	Step      float32
	Min       string
	Max       string
}

Input describes a piece of data the server needs from the client, including validation requirements.

It is analogous to HTML's <input> and <textarea>.

func (*Input) ExtractFormValue

func (i *Input) ExtractFormValue(form url.Values)

ExtractFormValue sets i.Value to the first value found at form[i.Name].

The found value is deleted from form.

func (Input) MarshalJSON

func (i Input) MarshalJSON() ([]byte, error)

func (Input) MarshalXML

func (i Input) MarshalXML(e *xml.Encoder, label xml.StartElement) error

func (*Input) ParseValueAsDate added in v0.6.0

func (i *Input) ParseValueAsDate() (t time.Time, err error)

ParseValueAsDate parses i.Value as `type="date"`. An ISO 8601 date.

Input.Type is not checked here.

func (*Input) ParseValueAsDatetime added in v0.6.0

func (i *Input) ParseValueAsDatetime() (t time.Time, err error)

ParseValueAsDatetime parses i.Value as `type="datetime"`. An ISO 8601 datetime that expects a timezone.

WARNING: This is not widely supported by browsers.

Input.Type is not checked here.

func (*Input) ParseValueAsDatetimeLocal added in v0.6.0

func (i *Input) ParseValueAsDatetimeLocal() (t time.Time, err error)

ParseValueAsDatetimeLocal parses i.Value as `type="datetime-local"`. An ISO 8601 datetime without a timezone.

Input.Type is not checked here.

func (*Input) ParseValueAsTime added in v0.6.0

func (i *Input) ParseValueAsTime() (t time.Time, err error)

ParseValueAsTime parses i.Value as `type="time"`. An ISO 8601 time.

Input.Type is not checked here.

func (*Input) Validate

func (p *Input) Validate() (err error)

Validate performs some basic checks on the value of the input according to its settings.

Input.Required, Input.Max, Input.Min, Input.MaxLength, and Input.MinLength are checked, in that order. Similar to the minimal checks that a browser would make for equivalent HTML.

Because it is complex to implement for whatever many types Input.Type might be set to, this function does not validate Input.Step.

Input.Type is treated as a hint to the user, Input.Value is not checked against Input.Type. Such validation is performed when a corresponding Input.ParseValueAs* method is called, although again, Input.Type is not checked, and any parse method may be called regardless of the type attribute. It is your responsibility to make sure that Input.Type is set according to how it is parsed.

This functionality can be extended with more bespoke validation by checking fields and setting the Input.Error field accordingly.

type Link struct {
	Label string `json:"label"`
	Href  string `json:"href"`
}

Link represents a state transition that requires no input—a simple navigation or action trigger.

func (Link) MarshalXML

func (i Link) MarshalXML(e *xml.Encoder, start xml.StartElement) error

type Map

type Map struct {
	Label string `json:"label"`
	Name  string `json:"name"`
	Error string `json:"error,omitempty"`
	// MaxEntries sets the maximum number of entries allowed when [Map.Validate] is called.
	//
	// Does nothing if set to zero or less.
	MaxEntries int `json:"max_entries,omitempty"`
	// MaxLength sets the maximum length that any given entry may be when [Map.Validate] is called.
	//
	// The length of an entry is the length of the key + the length of the value.
	//
	// Does nothing if set to zero or less.
	MaxLength int `json:"max_length,omitempty"`
	// MaxValues sets the maximum number of values allowed per entry when [Map.Validate] is called.
	//
	// Does nothing if set to zero or less.
	MaxValues int                 `json:"max_values,omitempty"`
	Entries   map[string][]string `json:"entries"`
}

Map represents an arbitrary set of key-value entries. Where Name == "foo", the form submission may populate Entries with values like this `foo[x]=y&foo[a]=b&foo[stuff]=etc`.

When Name == "", Map.ExtractFormValue will extract all values from the given form. Extract specific fields first to prevent them from being captured by the catch-all.

func (*Map) ExtractFormValue

func (m *Map) ExtractFormValue(form url.Values)

ExtractFormValue takes all entries from form with name x[y], where x = m.Name, and y is an arbitrary key provided by the request.

If m.Name == "", then all entries in form are transferred to m.Entries. Extract specific fields first to prevent them from being captured by the catch-all.

func (Map) MarshalXML

func (m Map) MarshalXML(e *xml.Encoder, label xml.StartElement) error

func (Map) NamedKey

func (m Map) NamedKey(key string) string

NamedKey returns key as a form name. E.g. where m.Name = "foo", m.NamedKey("bar") returns "foo[bar]"

If Name is not set, key is returned unchanged. E.g. where m.Name = "", m.NamedKey("bar") returns "bar"

func (*Map) Validate added in v0.6.0

func (m *Map) Validate() (err error)

Validate performs some basic checks on m.Entries according to given settings.

An error will be returned, and m.Error set, if any of m.MaxEntries, m.MaxLength, or m.MaxValues are violated.

type Namespace

type Namespace struct {
	HcXmlns string      `xml:"xmlns:c,attr" json:"-"`
	Docs    xml.Comment `xml:",comment" json:"-"`
}

Namespace provides the XML namespace declaration for hmc elements. It should be embedded in your top-level struct and initialized using NS().

Without the namespace, hmc elements (c:Form, c:Input, etc.) will not be properly recognized by browsers.

Example:

type MyPage struct {
	Namespace hmc.Namespace
	Title     string
	Form      hmc.Form[MyFormData]
}

page := MyPage{
	Namespace: hmc.NS(),
	Title:     "My Page",
}

This struct is omitted during JSON serialization.

func NS added in v0.6.0

func NS() Namespace

NS returns a Namespace with the correct namespace URI and documentation comment. Always use this function to initialize the Namespace field in your structs.

type Option

type Option struct {
	Label    string `json:"label,omitempty"`
	Value    string `json:"value"`
	Selected bool   `json:"selected,omitempty"`
	Disabled bool   `json:"disabled,omitempty"`
}

func (Option) MarshalXML

func (o Option) MarshalXML(e *xml.Encoder, start xml.StartElement) error

type Select

type Select struct {
	Label    string   `json:"label"`
	Multiple bool     `json:"multiple,omitempty"`
	Name     string   `json:"name"`
	Error    string   `json:"error,omitempty"`
	Required bool     `json:"required,omitempty"`
	Options  []Option `json:"options"`
	Disabled bool     `json:"disabled,omitempty"`
}

func (*Select) ExtractFormValue

func (s *Select) ExtractFormValue(form url.Values) (err error)

ExtractFormValue behaves similarly to Input.ExtractFormValue. If s.Multiple is set, all values are taken; if not, the first value is taken.

An error is returned if a value is extracted that is not listed in s.Options; but it is safe to ignore this error if unlisted selections are allowed. See Select.SetValues

func (Select) MarshalXML

func (i Select) MarshalXML(e *xml.Encoder, label xml.StartElement) error

func (*Select) SetValues

func (s *Select) SetValues(values ...string) (err error)

SetValues returns an error if a value is provided that is not listed in s.Options. Ignore this error if this Select is meant to allow unlisted selections.

func (Select) Value

func (s Select) Value() string

Returns the value of the first selected non-disabled option in s.Options.

See also Values() for getting all selected values when s.Multiple == true.

func (Select) Values

func (s Select) Values() iter.Seq[string]

Values returns a iterator of the values of all selected non-disabled options.

See also Value() for easily getting just the first selected value.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL