hal

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Apr 22, 2026 License: MIT Imports: 7 Imported by: 0

README

go-hal

A high-performance, strictly compliant HAL (Hypertext Application Language) library for Go. It focuses on zero-reflection runtime execution, type safety, and allocation-efficient HAL augmentation during JSON serialization.

Target Audience: This library is designed for senior Go developers building high-throughput microservices where HAL compliance and allocation-efficient JSON serialization are non-negotiable. It is not intended for beginners or those looking for an "all-in-one" web framework.

Minimal Example

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"os"

	// Alias the import to ensure the `hal` prefix is stable
	hal "github.com/Emin-ACIKGOZ/go-hal"
)

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

func init() {
	// 1. Register: Define links for *User (must be pointer type)
	hal.Register(func(ctx context.Context, u *User) []hal.Link {
		return []hal.Link{
			{Rel: "self", Href: fmt.Sprintf("/users/%d", u.ID)},
		}
	})
}

func main() {
	// 2. Wrap: Create the HAL envelope
	user := &User{ID: 101, Name: "Alice"}
	response := hal.Wrap(context.Background(), user)

	// 3. Marshal: HAL metadata injection occurs during marshaling
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	if err := enc.Encode(response); err != nil {
		panic(err)
	}

	// Output:
	// {
	//   "id": 101,
	//   "name": "Alice",
	//   "_links": {
	//     "self": { "href": "/users/101" }
	//   }
	// }
}

Non-Goals

To maintain speed, predictability, and simplicity, go-hal explicitly does not support:

  • Client-side Navigation This is a server-side serialization library only.

  • Code Generation No Go structs are generated from OpenAPI, and no OpenAPI is generated from Go code.

  • Router Integration Routes, HTTP methods, and URL construction are your responsibility. No Gin/Chi/Fiber inference.

  • Implicit Auto-Discovery Links are added only when you explicitly register a generator for a type.

Features

  • Zero-Reflection Runtime Generators are compiled into type-safe closures at registration time. No reflect.Call in the hot path.

  • Byte-Splicing Performance Injects _links and _embedded directly into JSON output without allocating intermediate maps.

  • Strict HAL Semantics Correct handling of single vs. multiple links per relation, and object-vs-array polymorphism.

  • Dual-Mode API Use a global singleton for convenience or isolated instances for unit testing.

Strict Mode

By default, hal.Wrap is permissive: if no generator is found for a type, the data is returned as-is.

Strict Mode exists to catch developer mistakes early. When enabled, it panics if:

  1. You wrap a value type T but registered a generator for *T.
  2. You wrap a type for which no generator exists at all.

Enable it during development or testing:

hal.DefaultInstance = hal.New(hal.WithStrictMode())

OpenAPI Adapter (hal/openapi)

The optional hal/openapi package bridges HAL with OpenAPI 3.0 schemas using kin-openapi.

What it does
  • Injects valid HAL _links and _embedded schemas into existing OpenAPI components
  • Models HAL link polymorphism (Link | []Link) correctly using oneOf
  • Allows contract-first teams to document HAL responses without hand-writing boilerplate
What it does not do
  • It does not generate OpenAPI specs from Go code
  • It does not validate runtime responses
  • It does not infer links from registered generators
Example
import (
	openapi "github.com/Emin-ACIKGOZ/go-hal/openapi"
)

// Using kin-openapi
adapter := openapi.New(doc)
adapter.InjectLinkSchema()
adapter.MakeResource(userSchema) // Augments schema with HAL fields

Migration Guide

This guide helps migrate from other HAL libraries to go-hal.

From raff/halgo (Go)

Before (halgo):

type User struct {
    halgo.Links
    Name string
}

user := User{
    Links: halgo.Links{}.Self("/users/1"),
    Name: "Alice",
}
json.Marshal(user)

After (go-hal):

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

hal.Register(func(ctx context.Context, u *User) []hal.Link {
    return []hal.Link{{Rel: "self", Href: fmt.Sprintf("/users/%d", u.ID)}}
})

env := hal.Wrap(ctx, &User{ID: 1, Name: "Alice"})
json.Marshal(env)
From pyhalboy (Python)

Before (pyhalboy):

resource = halboy.Resource()
resource.set_link('self', '/users/1')
resource.set_property('name', 'Alice')

After (go-hal):

hal.Register(func(ctx context.Context, u *User) []hal.Link {
    return []hal.Link{{Rel: "self", Href: fmt.Sprintf("/users/%d", u.ID)}}
})

env := hal.Wrap(ctx, &User{ID: 1, Name: "Alice"})
json.Marshal(env)
From Spring HATEOAS (Java)

Before (Spring):

User user = new User();
user.setId(1);
ControllerLinkBuilder.linkTo(UserController.class)
    .withRel("self")
    .add(user);

After (go-hal):

hal.Register(func(ctx context.Context, u *User) []hal.Link {
    return []hal.Link{{Rel: "self", Href: "/users/1"}}
})

env := hal.Wrap(ctx, &User{ID: 1})
json.Marshal(env)
Key Differences
Feature Other Libraries go-hal
Reflection Required Zero at runtime
Link injection At creation At JSON marshal
Dependencies Multiple go-json only
Type Safety Dynamic Compile-time verified

Performance Tuning

go-hal provides three optimization levels. Use the baseline for correctness; optimize only if profiling shows HAL injection is a bottleneck.

Performance Comparison

All benchmarks include JSON marshaling. Measurements are ns/op on amd64 Linux.

Method ns/op vs Baseline Best For
Register (dynamic) 5,960 baseline Links that depend on instance data
RegisterStatic 2,008 66% faster Links fixed per type
WrapPrecomputed 1,759 70% faster Pre-serialized links (cached/batch)
Raw JSON (no HAL) 659 — Performance ceiling (not applicable)

Note on hand-coded HAL: Hand-writing HAL response structs is ~4.6x faster than go-hal's baseline (1,289 ns/op), but requires manual struct maintenance, offers no type safety for link generators, and is error-prone in large codebases. go-hal's cost is justified for maintainability.

Optimization Levels
Level 1: Baseline (Default)

Use Register() for dynamic links that depend on runtime data.

hal.Register(func(ctx context.Context, u *User) []hal.Link {
    return []hal.Link{{Rel: "self", Href: fmt.Sprintf("/users/%d", u.ID)}}
})
env := hal.Wrap(ctx, u)
Level 2: Static Pre-computed

Use RegisterStatic() for links that are the same per type (just varying IDs).

inst := hal.New()
hal.RegisterStatic(inst, &User{}, []hal.Link{{Rel: "self", Href: "/users"}})

env := inst.Wrap(ctx, &User{ID: 42})
// Automatically uses pre-computed _links
Level 3: Maximum Performance

Use WrapPrecomputed() when you pre-serialize links yourself.

links := []byte(`{"self":{"href":"/users/42"}}`)
env := inst.WrapPrecomputed(ctx, user, links)

This bypasses all link generation for maximum throughput.

Memory Optimization

go-hal's byte-splicing avoids intermediate allocations:

Benchmark Allocations per Op
Baseline 14
RegisterStatic 5
WrapPrecomputed 5
When to Optimize
  1. RegisterStatic: Use when links are identical for every instance of a type (rare). Saves ~3,900 ns/op per resource.
  2. WrapPrecomputed: Use when you generate links in a separate pass (e.g., caching layer, batch API). Saves ~4,200 ns/op per resource.

Rule of thumb: Optimize only if profiling shows HAL injection >5% of request latency. For most APIs, baseline performance is acceptable (6 µs per resource).

Performance & Constraints

This library uses a deliberate byte-splicing technique to merge your struct's JSON output with HAL metadata.

Constraints
  1. Your data must marshal to a JSON object ({...}) Root arrays or primitives are not valid HAL resources.

  2. Your data must not be nil (unless you intentionally want an empty HAL response).

Debugging Tip

If you encounter invalid JSON (e.g. {,"_links":...}), inspect the raw JSON output of your data struct. go-hal relies on detecting the opening { and closing } of your payload.

License

MIT

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 standard HAL "_links" and "_embedded" fields without requiring changes to the underlying data models.

The package relies on a central or instance-based registry to map Go types to link generation functions. It supports CURIEs (Compact URIs) and strict mode validation to ensure type safety during link generation.

All methods on Instance are safe for concurrent use.

Package hal provides a high-performance HAL (Hypertext Application Language) implementation.

Overview

go-hal is a zero-reflection, allocation-efficient HAL serialization library for Go. It wraps your Go structs in HAL Envelopes, injecting _links and _embedded metadata during JSON serialization without intermediate allocations.

Key Features

  • Zero reflection at runtime (type-safe closures)
  • Pre-computed links for maximum performance
  • Full HAL spec compliance (draft-kelly-json-hal)
  • Dual-mode: global singleton or isolated instances

Quick Start

hal.Register(func(ctx context.Context, u *User) []hal.Link {
    return []hal.Link{{Rel: "self", Href: fmt.Sprintf("/users/%d", u.ID)}}
})

env := hal.Wrap(ctx, &User{ID: 1, Name: "Alice"})
json.Marshal(env) // => {"id":1,"name":"Alice","_links":{"self":{"href":"/users/1"}}}

Index

Constants

This section is empty.

Variables

View Source
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().

Functions

func GetVersion

func GetVersion() string

GetVersion returns the version string (e.g., "v1.0.0" or "v1.0.0-beta")

func Register

func Register[T any](gen func(context.Context, *T) []Link)

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.

Example

hal.RegisterCurie("acme", "https://docs.example.com/rels/{rel}")

func RegisterInstance

func RegisterInstance[T any](i *Instance, gen func(context.Context, *T) []Link)

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

func RegisterStatic(i *Instance, target any, links []Link)

RegisterStatic registers pre-computed links for a type. Links are serialized once at registration time, providing ~60% performance improvement over runtime generator calls. Best for static links that don't change per request.

Performance

Pre-computed links bypass generator invocation and JSON serialization on every request. This is optimal for links like "self" hrefs that only depend on the resource ID.

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"`
	Embedded map[string]any `json:"_embedded"`
	Count    int            `json:"count"`
	Total    int            `json:"total,omitempty"`
}

CollectionPage represents a standard HAL collection response. It includes navigation links, embedded items, and pagination metadata.

func Collection

func Collection[T any](ctx context.Context, items []*T, total int, selfLink Link) *CollectionPage

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 struct and injects _links and _embedded during JSON serialization.

To create an Envelope:

// Using global instance
env := hal.Wrap(ctx, &MyStruct{...})

// Using isolated instance
inst := hal.New()
env := inst.Wrap(ctx, &MyStruct{...})

The Envelope implements json.Marshaler and will inject HAL metadata automatically.

func Wrap

func Wrap(ctx context.Context, data any) *Envelope

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 (e *Envelope) AddLink(l Link)

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 the HAL specification.

func (*Envelope) MarshalJSON

func (e *Envelope) MarshalJSON() ([]byte, error)

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

type Generator func(ctx context.Context, v any) []Link

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.

This method panics if items is not a slice.

func (*Instance) RegisterCurie

func (i *Instance) RegisterCurie(prefix, href string)

RegisterCurie adds a CURIE (Compact URI) mapping to the instance. These are used to shorten link relations in the output JSON.

func (*Instance) RegisterInstance

func (i *Instance) RegisterInstance(gen any)

RegisterInstance registers a generator using reflection. Deprecated: Use the generic function RegisterInstance[T] instead.

func (*Instance) RegisteredTypes

func (i *Instance) RegisteredTypes() []reflect.Type

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

func (i *Instance) Wrap(ctx context.Context, data any) *Envelope

Wrap creates a HAL Envelope with computed links. It first checks for pre-computed links (via RegisterStatic), then falls back to generators. This is the primary method for wrapping data in HAL envelopes.

Automatic Optimization

If RegisterStatic was called for the data's type, links are pre-computed and this method automatically uses them, providing ~60% better performance.

func (*Instance) WrapPrecomputed

func (i *Instance) WrapPrecomputed(_ context.Context, data any, linksJSON []byte) *Envelope

WrapPrecomputed wraps data with pre-serialized links JSON. This is the absolute fastest option - bypasses all generator calls.

Performance

Provides ~65% better performance than Wrap by eliminating:

  • Generator invocation
  • JSON serialization of links

Input Format

linksJSON must be a JSON object containing link relations:

{"self": {"href": "/users/42"}}

Example

links := []byte(`{"self":{"href":"/users/42"}})
env := inst.WrapPrecomputed(ctx, &User{ID: 42}, links)
json.Marshal(env)

func (*Instance) WrapRaw

func (i *Instance) WrapRaw(data any) *Envelope

WrapRaw creates an envelope without computing links. Use this for cases where context-based link generation is not needed.

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. When enabled, hal.Wrap panics if:

  1. You pass a value type T but registered a generator for *T
  2. You pass a type with no registered generator

This helps catch type mismatch errors during development.

Example

inst := hal.New(hal.WithStrictMode()) // panics on mismatches
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.

Link{
    Rel:       "self",
    Href:      "/users/123",
    Templated: false,
    Title:     "Current User",
}
type PrecomputedLinks struct {
	JSON []byte
}

PrecomputedLinks stores pre-computed links JSON

type VersionInfo

type VersionInfo struct {
	Major      int
	Minor      int
	Patch      int
	PreRelease string
}

VersionInfo holds version metadata

func GetVersionInfo

func GetVersionInfo() VersionInfo

GetVersionInfo returns detailed version information

Directories

Path Synopsis
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.

Jump to

Keyboard shortcuts

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