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" and "_embedded" fields without requiring changes to the underlying data models. AddEmbedded supports arbitrary Resource Objects and the §8.4 cache pattern; CollectionPage also emits the project-specific "_embedded.items" convention.
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, stable link shapes, media-type helpers, and independent opt-in strict policies for links, relations, CURIEs, and JSON. Template hrefs gain templated=true on emission, and generated documents declare CURIEs for embedded-only relation prefixes. Client-side decoding and navigation are out of scope for this server-side serializer: draft-11 has no client-side MUST, makes the hypertext cache pattern client-optional (§8.4), and places the deprecation notification SHOULD on clients that traverse a link (§5.4).
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 ¶
- Constants
- Variables
- func ContentType(profiles ...string) (string, error)
- 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)
- func ValidateLink(l Link) error
- type CollectionPage
- type DuplicateKeyError
- 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
- func WithLinkShape(shape LinkShape) InstanceOption
- func WithRelationShape(rel string, shape LinkShape) InstanceOption
- func WithStrictCuries() InstanceOption
- func WithStrictJSON() InstanceOption
- func WithStrictLinks() InstanceOption
- func WithStrictMode() InstanceOption
- func WithStrictRelations() InstanceOption
- type Link
- type LinkShape
- type PrecomputedLinks
- type VersionInfo
Constants ¶
const MediaTypeHAL = "application/hal+json"
MediaTypeHAL is the media type named by draft-kelly-json-hal-11. It is not registered with IANA.
const MediaTypeVendorHAL = "application/vnd.hal+json"
MediaTypeVendorHAL is the IANA-registered vendor HAL media type. Its 2011 IANA registration defines no optional parameters.
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 ErrDuplicateJSONKey = errors.New("hal: duplicate JSON key")
ErrDuplicateJSONKey is returned when JSON contains duplicate object member names. Duplicate names are non-interoperable and cannot be detected after decoding into a map.
var ErrEmbeddedDepthExceeded = errors.New("hal: embedded resource depth exceeded")
ErrEmbeddedDepthExceeded is returned when embedded envelopes form a cycle or exceed the maximum nesting depth during marshaling.
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 unusable name (empty, whitespace/control-bearing, non-UTF-8, colon-bearing, or failing the strict ASCII NCName subset), an empty href, or a strict href/templated violation.
var ErrInvalidEmbedded = errors.New("hal: invalid embedded resource")
ErrInvalidEmbedded is returned when an embedded value cannot be represented as a HAL Resource Object.
var ErrInvalidLink = errors.New("hal: invalid link")
ErrInvalidLink is returned when a Link fails validation. By default links are permissive; ValidateLink and instances configured with WithStrictLinks apply the full checks.
var ErrInvalidProfile = errors.New("hal: invalid profile")
ErrInvalidProfile is returned when a ContentType profile is not an absolute URI. It is specific to ContentType; strict Link.Profile validation wraps ErrInvalidLink instead.
var ErrInvalidRelation = errors.New("hal: invalid link relation")
ErrInvalidRelation is returned for a blank, control-bearing, non-UTF-8, or otherwise unusable link relation.
var ErrLinkShape = errors.New("hal: incompatible link shape")
ErrLinkShape is returned when a configured shape cannot represent the links present, such as ShapeObject with repeated links.
Functions ¶
func ContentType ¶ added in v1.3.0
ContentType builds an application/hal+json content type with an optional RFC 6906 profile parameter. Zero profiles returns the bare media type; each non-empty profile must be an absolute URI and is joined into one space- separated profile value. The bytes are preserved as supplied; schemes other than http/https (urn:, mailto:, tag:) are accepted.
func GetVersion ¶
func GetVersion() string
GetVersion returns the version string (e.g., "v1.4.0" or "v1.4.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 for an unusable prefix (empty, whitespace/control-bearing, non-UTF-8, colon-bearing, or failing the strict NCName subset), an empty href, or a strict href/templated violation.
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})
func ValidateLink ¶ added in v1.3.0
ValidateLink validates a Link Object without requiring a relation namespace: href URI/template consistency and the optional type, deprecation, profile, and hreflang fields. Rel, name, title, and method are not validated.
ValidateLink validates the normalized, as-emitted link: a supported template href derives templated automatically. Callers that marshal a Link struct directly, outside an Envelope, must set Templated themselves.
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 DuplicateKeyError ¶ added in v1.3.0
DuplicateKeyError reports the first duplicate object key and its byte offset.
func (*DuplicateKeyError) Error ¶ added in v1.3.0
func (e *DuplicateKeyError) Error() string
func (*DuplicateKeyError) Unwrap ¶ added in v1.3.0
func (e *DuplicateKeyError) Unwrap() error
Unwrap supports errors.Is(err, ErrDuplicateJSONKey).
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) AddCachedLink ¶ added in v1.3.0
AddCachedLink adds a link and embeds the same relation's resource, the HAL hypertext cache pattern from draft §8.4. The operation is atomic: if the relation or resource is invalid, neither the link nor the embedded value is added. On static or precomputed envelopes it records the deferred ErrAddLinkPrecomputed and adds nothing.
func (*Envelope) AddEmbedded ¶ added in v1.3.0
AddEmbedded appends an embedded resource under rel, following HAL §4.1.2.
One value is emitted as an object; repeated values form an array. A slice is flattened one level, a one-element slice is emitted as an object, and an empty slice is a no-op. Nested envelopes must be *Envelope values so the marshaller can bound depth and detect cycles without copying identity.
Raw structs, maps, and object json.RawMessage values are embedded as-is; they are not automatically wrapped or link-generated. Nil, typed-nil, scalar, []byte, Envelope values, and non-object RawMessage values are rejected with a deferred ErrInvalidEmbedded.
AddEmbedded is a pointer method and is not safe to call concurrently with marshaling. It works on generated, raw, static, and precomputed envelopes.
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. The value receiver is preserved so Envelope values and *Envelope pointers both marshal; direct *Envelope children are marshaled manually with a shared depth and ancestor scope.
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 for an unusable prefix (empty, whitespace/control-bearing, non-UTF-8, colon-bearing, or failing the strict NCName subset), an empty href, or a strict href/templated violation. Generated declarations derive templated from the href template.
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 WithLinkShape ¶ added in v1.3.0
func WithLinkShape(shape LinkShape) InstanceOption
WithLinkShape sets the default relation shape. ShapeAuto keeps the default object-for-one/array-for-many behavior; ShapeArray always emits arrays and is the draft §4.1.1 stable-shape profile; ShapeObject requires at most one link per relation.
func WithRelationShape ¶ added in v1.3.0
func WithRelationShape(rel string, shape LinkShape) InstanceOption
WithRelationShape overrides the default shape for one relation. Later calls for the same relation replace the earlier override.
func WithStrictCuries ¶ added in v1.3.0
func WithStrictCuries() InstanceOption
WithStrictCuries enables strict CURIE declaration validation on AddLink, merge, RegisterCurie, RegisterStatic, and precomputed wrapping. It is independent of WithStrictMode.
Strict names use the ASCII NCName subset [A-Za-z_][A-Za-z0-9._-]*.
func WithStrictJSON ¶ added in v1.3.0
func WithStrictJSON() InstanceOption
WithStrictJSON enables duplicate-key scanning of marshaled Resource Objects. It scans each envelope's non-empty Data object and raw embedded values, and the policy is inherited by embedded child envelopes (the effective policy is parent OR child). It does not rescan final output or precomputed/static link JSON (those are scanned at compile time). DuplicateKeyError.Offset is relative to the scanned value, not the final document. Default marshaling stays permissive.
func WithStrictLinks ¶ added in v1.3.0
func WithStrictLinks() InstanceOption
WithStrictLinks enables link validation on AddLink, generated links, static links, Collection self links, and precomputed wrapping. It is independent of WithStrictMode. By default links are permissive; ValidateLink can be called directly for one-off checks.
Strict links accept absolute and relative URI references, derive templated=true for supported template hrefs, require templated=false/absent for URI hrefs, reject empty/whitespace-only hrefs, and validate type, deprecation, profile, and hreflang syntax.
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
func WithStrictRelations ¶ added in v1.3.0
func WithStrictRelations() InstanceOption
WithStrictRelations enables strict relation grammar on AddLink, AddEmbedded, generated links, static links, and precomputed wrapping. It is independent of WithStrictMode.
Strict relations accept lowercase RFC 8288 reg-rel-type tokens, any absolute URI scheme (including urn:, mailto:, and tag:), and declared CURIEs. Relation grammar is checked lexically; IANA registration status is not consulted. Relative references, ":x", and mixed-case bare tokens are rejected. An undeclared "acme:widget" is accepted as an opaque URI because a non-authority scheme form is indistinguishable from an absolute URI without declaration context.
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 LinkShape ¶ added in v1.3.0
type LinkShape uint8
LinkShape controls whether a relation is serialized as one object or always as an array. ShapeAuto keeps the HAL default: one value is an object and repeated values form an array.
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 structurally validated handle that can be reused across envelopes and instances. It does not opt into strict link or CURIE semantics.
func CompilePrecomputedLinksStrict ¶ added in v1.3.0
func CompilePrecomputedLinksStrict(linksJSON []byte) (*PrecomputedLinks, error)
CompilePrecomputedLinksStrict is CompilePrecomputedLinks plus strict link and strict CURIE validation. The resulting handle can be wrapped by strict instances without revalidation.
type VersionInfo ¶
VersionInfo holds version metadata
func GetVersionInfo ¶
func GetVersionInfo() VersionInfo
GetVersionInfo returns detailed version information
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
relation
Package relation contains the single relation classifier shared by link, embedded-resource, and CURIE handling.
|
Package relation contains the single relation classifier shared by link, embedded-resource, and CURIE handling. |
|
uriref
Package uriref validates RFC 3986 URI references conservatively.
|
Package uriref validates RFC 3986 URI references conservatively. |
|
uritemplate
Package uritemplate parses RFC 6570 URI Templates into a small AST.
|
Package uritemplate parses RFC 6570 URI Templates into a small AST. |
|
Package openapi provides an adapter to augment OpenAPI 3.0 documents with HAL semantics.
|
Package openapi provides an adapter to augment OpenAPI 3.0 documents with HAL semantics. |