Documentation
¶
Overview ¶
Package hal implements the Hypertext Application Language (HAL) for JSON.
It provides a mechanism to wrap existing Go structs in an envelope that injects HAL "_links" without requiring changes to the underlying data models. CollectionPage also emits the project-specific "_embedded.items" convention; arbitrary embedded resources are not exposed.
The package relies on a central or instance-based registry to map Go types to link generation functions. It supports CURIEs (Compact URIs), reusable precomputed links, and strict-mode checks on the generated path.
The target is the supported subset of draft-kelly-json-hal-11, an expired Informational draft rather than an RFC. See COMPLIANCE.md for the support matrix, validation boundaries, and out-of-scope features.
All methods on Instance are safe for concurrent use. Envelopes may be marshaled concurrently while they are not being mutated.
Index ¶
- Variables
- func GetVersion() string
- func Register[T any](gen func(context.Context, *T) []Link)
- func RegisterCurie(prefix, href string)
- func RegisterInstance[T any](i *Instance, gen func(context.Context, *T) []Link)
- func RegisterStatic(i *Instance, target any, links []Link)
- type CollectionPage
- type Envelope
- type Generator
- type Instance
- func (i *Instance) Collection(ctx context.Context, items any, total int, selfLink Link) *CollectionPage
- func (i *Instance) RegisterCurie(prefix, href string)
- func (i *Instance) RegisterInstance(gen any)
- func (i *Instance) RegisteredTypes() []reflect.Type
- func (i *Instance) Wrap(ctx context.Context, data any) *Envelope
- func (i *Instance) WrapPrecomputed(_ context.Context, data any, linksJSON []byte) *Envelope
- func (i *Instance) WrapPrecomputedLinks(data any, links *PrecomputedLinks) *Envelope
- func (i *Instance) WrapRaw(data any) *Envelope
- type InstanceOption
- type Link
- type PrecomputedLinks
- type VersionInfo
Constants ¶
This section is empty.
Variables ¶
var DefaultInstance = New()
DefaultInstance is the global singleton registry used by package-level functions. If you need isolated registries for testing, create your own with New().
var ErrAddLinkPrecomputed = errors.New("hal: cannot add links to a static or precomputed envelope")
ErrAddLinkPrecomputed is returned when AddLink is called on an envelope whose links are static or precomputed. Those links are immutable in the envelope.
var ErrDuplicateCurie = errors.New("hal: duplicate CURIE declaration")
ErrDuplicateCurie is returned when CURIE declarations in one source reuse a name. Duplicate declarations are ambiguous and not interoperable.
var ErrHALFieldConflict = errors.New("hal: data already contains reserved HAL field")
ErrHALFieldConflict is returned when user data already contains a reserved top-level HAL property. Emitting those fields would create duplicate JSON object keys, which are not interoperable.
var ErrInvalidCurie = errors.New("hal: invalid CURIE declaration")
ErrInvalidCurie is returned when a CURIE declaration has an empty or whitespace-bearing name, or an empty href.
Functions ¶
func GetVersion ¶
func GetVersion() string
GetVersion returns the version string (e.g., "v1.2.0" or "v1.2.0-beta")
func Register ¶
Register registers a generator function for type T on the DefaultInstance. The generator is called at runtime to produce links for wrapped resources.
Type Requirements ¶
- T must be a pointer type (e.g., *User not User)
- Generators are registered for the pointee type
Example ¶
hal.Register(func(ctx context.Context, u *User) []Link {
return []Link{
{Rel: "self", Href: fmt.Sprintf("/users/%d", u.ID)},
{Rel: "edit", Href: fmt.Sprintf("/users/%d/edit", u.ID)},
}
})
func RegisterCurie ¶
func RegisterCurie(prefix, href string)
RegisterCurie registers a CURIE (Compact URI) prefix on the DefaultInstance. CURIEs allow shortened link relations like "acme:widget" that expand to full URIs. It panics if prefix is empty or contains whitespace, or if href is empty.
Example ¶
hal.RegisterCurie("acme", "https://docs.example.com/rels/{rel}")
func RegisterInstance ¶
RegisterInstance binds a strongly-typed generator function to the provided Instance. The generator will be invoked whenever Wrap is called with a value of type *T.
func RegisterStatic ¶
RegisterStatic registers pre-computed links for a type. Links are serialized once at registration time and resolve registered CURIEs at wrap time. Best for links that do not change per resource instance.
It panics for a nil instance, a nil or non-pointer target, an empty relation, or invalid/duplicate CURIE declarations. A typed-nil pointer target such as (*T)(nil) is accepted. Passing no links registers an empty link set that suppresses generators and emits no _links.
Example ¶
inst := hal.New()
hal.RegisterStatic(inst, &User{}, []hal.Link{
{Rel: "self", Href: "/users"},
})
// At runtime - uses pre-computed links automatically
env := inst.Wrap(ctx, &User{ID: 42})
Types ¶
type CollectionPage ¶
type CollectionPage struct {
Links map[string]any `json:"_links,omitempty"`
Embedded map[string]any `json:"_embedded,omitempty"`
Count int `json:"count"`
Total int `json:"total"`
}
CollectionPage represents a project-specific HAL collection convention. It includes navigation links, embedded items, and pagination metadata.
Count and Total are always serialized so a total of zero stays observable; they are not part of the HAL specification. Empty _links and _embedded maps are omitted.
func Collection ¶
Collection creates a CollectionPage using the DefaultInstance.
type Envelope ¶
type Envelope struct {
Data any // The user's struct
// contains filtered or unexported fields
}
Envelope is the container for your data with HAL metadata. It wraps your Go value and injects _links during JSON serialization. CollectionPage separately emits the project-specific _embedded.items shape.
To create an Envelope:
// Using global instance
env := hal.Wrap(ctx, &MyStruct{...})
// Using isolated instance
inst := hal.New()
env := inst.Wrap(ctx, &MyStruct{...})
Envelope implements json.Marshaler with a value receiver, so both Envelope and *Envelope marshal as HAL resources. AddLink remains pointer-only. Static and precomputed envelopes reject AddLink with ErrAddLinkPrecomputed. An Envelope may be marshaled repeatedly or concurrently while it is not being mutated.
func Wrap ¶
Wrap wraps data in a HAL Envelope using the DefaultInstance. The envelope will inject _links during JSON serialization based on registered generators.
Example ¶
user := &User{ID: 42, Name: "Alice"}
env := hal.Wrap(context.Background(), user)
json.Marshal(env)
func (*Envelope) AddLink ¶
AddLink appends a link to the envelope. If a link with the same Relation (Rel) already exists, it is converted to a slice of links as per HAL relation multiplicity.
AddLink is a pointer method. On static or precomputed envelopes it does not add the link and defers ErrAddLinkPrecomputed to marshaling. An empty or whitespace-only relation is also deferred as an error. It is not safe to call AddLink concurrently with marshaling.
func (Envelope) MarshalJSON ¶
MarshalJSON implements the json.Marshaler interface. It serializes the wrapped Data and splices in the HAL "_links" and "_embedded" fields into the resulting JSON object.
type Generator ¶
Generator defines a function that produces HAL links for a specific type. The function receives the data to generate links for and returns a slice of Link objects.
Example ¶
Generator: func(ctx context.Context, u *User) []Link {
return []Link{{Rel: "self", Href: fmt.Sprintf("/users/%d", u.ID)}}
}
type Instance ¶
type Instance struct {
// contains filtered or unexported fields
}
Instance maintains a registry of link generators and CURIE definitions. It is safe for concurrent use.
Creating an Instance ¶
// Basic instance inst := hal.New() // With strict mode inst := hal.New(hal.WithStrictMode())
func New ¶
func New(opts ...InstanceOption) *Instance
New creates a new HAL Instance.
The instance maintains its own registry of generators, allowing for isolated testing. Options can be provided to configure the instance:
inst := hal.New(hal.WithStrictMode())
func (*Instance) Collection ¶
func (i *Instance) Collection(ctx context.Context, items any, total int, selfLink Link) *CollectionPage
Collection wraps a slice of items into a HAL CollectionPage. It iterates over the items, wraps each one using the registered generators, and constructs the embedded items list. Generators are registered for *T, so use []*T to get per-item links; []T does not match a *T generator.
This method panics if items is not a slice.
func (*Instance) RegisterCurie ¶
RegisterCurie adds a CURIE (Compact URI) mapping to the instance. These are used to shorten link relations in the output JSON.
It panics if prefix is empty or contains whitespace, or if href is empty.
func (*Instance) RegisterInstance ¶
RegisterInstance registers a generator using reflection. Deprecated: Use the generic function RegisterInstance[T] instead.
func (*Instance) RegisteredTypes ¶
RegisteredTypes returns a list of all Go types that have a generator registered. This is a Read-Only introspection hook useful for adapters (like OpenAPI).
func (*Instance) Wrap ¶
Wrap creates a HAL Envelope with computed links. It first checks for pre-computed links (via RegisterStatic), then falls back to generators. If static links were registered for the data's type, those links take precedence and generators are not called.
func (*Instance) WrapPrecomputed ¶
WrapPrecomputed wraps data with caller-supplied links JSON. It validates and copies the input and computes the CURIE prefixes on every call. For a hot path, validate once with CompilePrecomputedLinks and reuse the handle with WrapPrecomputedLinks.
Input Format ¶
linksJSON may be raw link relations or the wrapped form:
{"self": {"href": "/users/42"}}
{"_links": {"self": {"href": "/users/42"}}}
Invalid input produces a deferred error during marshaling.
Example ¶
links := []byte(`{"self":{"href":"/users/42"}}`)
env := inst.WrapPrecomputed(ctx, &User{ID: 42}, links)
json.Marshal(env)
func (*Instance) WrapPrecomputedLinks ¶ added in v1.1.0
func (i *Instance) WrapPrecomputedLinks(data any, links *PrecomputedLinks) *Envelope
WrapPrecomputedLinks wraps data with an already-compiled link handle. The handle is reusable across envelopes and instances; its JSON is not copied or revalidated. A nil handle has WrapRaw semantics, while a non-nil zero handle means an empty link set.
type InstanceOption ¶
type InstanceOption func(*Instance)
InstanceOption configures a new HAL Instance. Apply options when creating a new Instance:
inst := hal.New(hal.WithStrictMode())
func WithStrictMode ¶
func WithStrictMode() InstanceOption
WithStrictMode enables panic on generator mismatches.
It applies only to Instance.Wrap on the generated path. When Data is a non-nil struct or pointer-to-struct, Wrap panics if:
- You pass a value type T but registered a generator for *T
- No generator is registered for the exact type
Static links, WrapRaw, WrapPrecomputed, WrapPrecomputedLinks, nil data, and non-struct values bypass these checks. Strict mode does not validate links or JSON.
Example ¶
inst := hal.New(hal.WithStrictMode()) // panics on generator mismatches
type Link ¶
type Link struct {
Rel string `json:"-"` // Used for map keys, not serialized directly
Href string `json:"href"`
Templated bool `json:"templated,omitempty"`
Type string `json:"type,omitempty"`
Deprecation string `json:"deprecation,omitempty"` // OPTIONAL: link to deprecation info
Name string `json:"name,omitempty"`
Profile string `json:"profile,omitempty"` // OPTIONAL: URI hint about target
Title string `json:"title,omitempty"`
HrefLang string `json:"hreflang,omitempty"` // OPTIONAL: language of target
Method string `json:"method,omitempty"` // Non-standard: common hint for HTTP methods
}
Link represents a HAL Hypermedia link. Implements draft-kelly-json-hal Section 5 Link Object.
All fields (except Rel and Method) are compliant with the HAL specification. Rel is used internally as the map key and is not serialized. Method is a non-standard extension for HTTP method hints.
Example ¶
Link{
Rel: "self",
Href: "/users/123",
Templated: false,
Title: "Current User",
}
type PrecomputedLinks ¶
type PrecomputedLinks struct {
JSON []byte // wrapped {"_links":...}; read-only; owned by handle
// contains filtered or unexported fields
}
PrecomputedLinks holds validated, wrapped pre-serialized links JSON.
The JSON field is read-only by convention and owned by the handle. Do not mutate it after compilation. Its zero value means an empty link set with no _links field. Use CompilePrecomputedLinks to build a handle from caller JSON.
func CompilePrecomputedLinks ¶ added in v1.1.0
func CompilePrecomputedLinks(linksJSON []byte) (*PrecomputedLinks, error)
CompilePrecomputedLinks validates and copies caller-supplied link JSON once, producing a handle that can be reused across envelopes and instances.
type VersionInfo ¶
VersionInfo holds version metadata
func GetVersionInfo ¶
func GetVersionInfo() VersionInfo
GetVersionInfo returns detailed version information