horos

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Mar 1, 2026 License: MIT Imports: 8 Imported by: 0

README

horos — type system et contrats de service

horos fournit des contrats de service typés au-dessus de connectivity.Handler, un format envelope wire (2B format + 4B CRC-32C + payload), et des erreurs structurées inter-services.

Quick start

// Définir un contrat typé.
var SearchContract = horos.Contract[SearchReq, SearchResp]{
    Service: "domkeeper_search", FormatID: 1,
}

// Appel typé (remplace json.Marshal + router.Call + json.Unmarshal).
resp, err := SearchContract.Call(ctx, router, req)

// Handler typé (remplace func(ctx, []byte) ([]byte, error)).
router.RegisterLocal("domkeeper_search", SearchContract.Handler(svc.search))

Wire format

┌──────────────┬──────────────┬───────────┐
│ format_id    │ CRC-32C      │ payload   │
│ (2B uint16LE)│ (4B uint32LE)│ (variable)│
└──────────────┴──────────────┴───────────┘

Formats built-in : 0=raw, 1=JSON, 2=msgpack.

Exported API

Symbol Description
Codec[T] Interface générique F-bounded (Encode/Decode)
Contract[Req, Resp] Contrat typé : Call + Handler
ServiceError Erreur structurée traversant le fil (__error sentinel)
Registry Registre de formats (source de vérité Go)
Wrap(formatID, data) Emballe avec envelope
Unwrap(data) Déballe + vérifie CRC

Quand utiliser

Couche optionnelle au-dessus de connectivity.Handler pour éliminer le JSON manuel. Utile quand un service a beaucoup de handlers ou quand on veut du type-safety compile-time.

Anti-patterns

Ne pas faire Faire
Retourner error Go depuis un handler Contract ServiceError pour traverser le fil
Ignorer le CRC-32C dans un parser custom Toujours vérifier
Deux formats avec le même ID Erreur à l'enregistrement

Documentation

Overview

Package horos provides the HOROS type system: typed service contracts, codec-agnostic serialization, structured errors, and a wire envelope format for inter-service communication.

The type system sits above connectivity.Handler (bytes in, bytes out) and provides compile-time safety for what travels inside those bytes.

Index

Constants

View Source
const (
	// HeaderSize is the fixed overhead of the wire envelope.
	HeaderSize = 6 // 2 (format_id) + 4 (checksum)

	// FormatRaw is passthrough — no codec, payload is used as-is.
	FormatRaw uint16 = 0

	// FormatJSON is the JSON codec format ID.
	// JSON is format 1: the canonical, human-readable wire format.
	// Use for debugging, external consumers, or when readability matters.
	FormatJSON uint16 = 1

	// FormatMsgp is the MessagePack codec format ID.
	// Msgpack is format 2: the performance-optimized wire format for
	// Go-to-Go inter-service communication. Keys are in cleartext in
	// the encoding, so it remains inspectable unlike protobuf.
	FormatMsgp uint16 = 2
)
View Source
const Schema = `` /* 163-byte string literal not displayed */

Schema is the SQLite DDL for the formats table. This table mirrors the Go-side registry for observability, admin UI, and format negotiation.

Variables

View Source
var (
	ErrNotFound    = &ServiceError{Code: "NOT_FOUND"}
	ErrBadRequest  = &ServiceError{Code: "BAD_REQUEST"}
	ErrInternal    = &ServiceError{Code: "INTERNAL"}
	ErrRateLimited = &ServiceError{Code: "RATE_LIMITED"}
	ErrForbidden   = &ServiceError{Code: "FORBIDDEN"}
	ErrConflict    = &ServiceError{Code: "CONFLICT"}
)

Common error codes as pre-built ServiceError values for use with errors.Is.

Functions

func IsChecksumError

func IsChecksumError(err error) bool

IsChecksumError returns true if the error is a checksum mismatch.

func Unwrap

func Unwrap(data []byte) (formatID uint16, payload []byte, err error)

Unwrap extracts the format ID and payload from a wire envelope, verifying the checksum. If the data has no envelope header (too short or format_id=0), it is returned as raw payload with FormatRaw.

func Wrap

func Wrap(formatID uint16, payload []byte) ([]byte, error)

Wrap creates a wire envelope from a format ID and payload. The envelope is always prepended, even for FormatRaw, so that Unwrap can unambiguously detect the format on the receiving end.

Types

type Codec

type Codec[T any] interface {
	Encode() ([]byte, error)
	Decode([]byte) (T, error)
}

Codec defines how a type serializes itself to and from bytes. The F-bounded constraint T Codec[T] ensures that Decode returns the concrete type, not an interface — zero runtime casts, zero panics.

The F-bounded constraint T Codec[T] has been supported since Go 1.18 (generics introduction).

type Contract

type Contract[Req Codec[Req], Resp Codec[Resp]] struct {
	// Service is the name used for routing in connectivity.Router.
	Service string

	// FormatID identifies the wire format (1=JSON, 2=msgpack).
	// When zero, the registry default is used.
	FormatID uint16
}

Contract is a typed service contract: it binds a request type and a response type together with a service name. Both Req and Resp must be self-codecs.

A Contract is metadata — it carries no state and exists to give the compiler enough information to type-check service calls at build time.

func NewContract

func NewContract[Req Codec[Req], Resp Codec[Resp]](service string) Contract[Req, Resp]

NewContract creates a contract for a service with the default wire format.

func (Contract[Req, Resp]) Call

func (c Contract[Req, Resp]) Call(
	ctx context.Context,
	caller func(ctx context.Context, service string, payload []byte) ([]byte, error),
	req Req,
) (Resp, error)

Call encodes req, dispatches via the provided caller function, and decodes the response. The caller is typically connectivity.Router.Call or a test stub.

This is the typed entry point: callers get compile-time guarantees on both the request and response types.

func (Contract[Req, Resp]) Handler

func (c Contract[Req, Resp]) Handler(
	fn func(ctx context.Context, req Req) (Resp, error),
) func(ctx context.Context, payload []byte) ([]byte, error)

Handler returns a connectivity-compatible handler (bytes in, bytes out) from a typed endpoint function. This is the server side of a contract.

func (Contract[Req, Resp]) WithFormat

func (c Contract[Req, Resp]) WithFormat(id uint16) Contract[Req, Resp]

WithFormat returns a copy of the contract using the specified format ID.

type Encoder

type Encoder interface {
	Encode() ([]byte, error)
}

Encoder writes a value to bytes. This is the write-only side of Codec, useful when a function only needs to serialize (e.g. building a request).

type ErrChecksum

type ErrChecksum struct {
	Expected uint32
	Actual   uint32
	FormatID uint16
}

ErrChecksum is returned when the wire envelope checksum doesn't match.

func (*ErrChecksum) Error

func (e *ErrChecksum) Error() string

type ErrDecode

type ErrDecode struct {
	Service string
	Cause   error
}

ErrDecode is returned when request/response decoding fails.

func (*ErrDecode) Error

func (e *ErrDecode) Error() string

func (*ErrDecode) Unwrap

func (e *ErrDecode) Unwrap() error

type ErrEncode

type ErrEncode struct {
	Service string
	Cause   error
}

ErrEncode is returned when request/response encoding fails.

func (*ErrEncode) Error

func (e *ErrEncode) Error() string

func (*ErrEncode) Unwrap

func (e *ErrEncode) Unwrap() error

type ErrUnsupportedFormat

type ErrUnsupportedFormat struct {
	FormatID uint16
}

ErrUnsupportedFormat is returned when a format ID has no registered codec.

func (*ErrUnsupportedFormat) Error

func (e *ErrUnsupportedFormat) Error() string

type FormatInfo

type FormatInfo struct {
	// ID is the format identifier used in the wire envelope header.
	ID uint16

	// Name is a human-readable label (e.g. "json", "msgpack", "protobuf").
	Name string

	// MIME is the content type (e.g. "application/json", "application/msgpack").
	MIME string
}

FormatInfo describes a registered wire format.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry maps format IDs to human-readable names and tracks which formats are available. The Go-side registry is the source of truth for codec dispatch; the SQLite table is for observability and admin tooling.

Registry is safe for concurrent use.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates a registry with the built-in formats pre-registered.

func (*Registry) All

func (r *Registry) All() []FormatInfo

All returns a snapshot of all registered formats.

func (*Registry) InitDB

func (r *Registry) InitDB(db *sql.DB) error

InitDB creates the formats table and seeds it with built-in formats.

func (*Registry) Lookup

func (r *Registry) Lookup(id uint16) (FormatInfo, bool)

Lookup returns the format info for the given ID, or false if not found.

func (*Registry) Register

func (r *Registry) Register(info FormatInfo) error

Register adds a format to the registry. Returns an error if the ID is already registered with a different name (prevents accidental collisions).

func (*Registry) SyncToDB

func (r *Registry) SyncToDB(db *sql.DB) error

SyncToDB writes all registered formats to the SQLite table (upsert). Call this after registering new formats at runtime.

type ServiceError

type ServiceError struct {
	// Code is a machine-readable error code (e.g. "NOT_FOUND", "RATE_LIMITED").
	Code string `json:"code"`

	// Message is a human-readable description.
	Message string `json:"message"`

	// Details is optional structured data for the error (retry-after, field
	// validation errors, etc.). It is codec-dependent: JSON for now.
	Details json.RawMessage `json:"details,omitempty"`

	// Service is the originating service name (set by the handler).
	Service string `json:"service,omitempty"`
}

ServiceError is a structured error that travels across the wire. Unlike Go's error interface (a string), ServiceError carries a machine-readable code, a human message, and optional typed details — all serializable.

ServiceError implements Codec[ServiceError] so it can be encoded in the same wire envelope as any other payload.

func DetectError

func DetectError(payload []byte) (*ServiceError, bool)

DetectError checks if a payload contains a ServiceError (by looking for the __error sentinel). Returns the error and true if found.

func NewServiceError

func NewServiceError(code, message string) *ServiceError

NewServiceError creates a ServiceError with the given code and message.

func ToServiceError

func ToServiceError(err error) *ServiceError

ToServiceError converts any error to a ServiceError. If the error is already a *ServiceError, it is returned as-is. Otherwise, a generic INTERNAL error is created.

func (ServiceError) Decode

func (e ServiceError) Decode(data []byte) (ServiceError, error)

Decode deserializes a ServiceError from JSON.

func (ServiceError) Encode

func (e ServiceError) Encode() ([]byte, error)

Encode serializes the ServiceError to JSON with the __error sentinel.

func (*ServiceError) Error

func (e *ServiceError) Error() string

Error implements the error interface.

func (*ServiceError) Is

func (e *ServiceError) Is(target error) bool

Is supports errors.Is matching by code.

func (*ServiceError) WithDetails

func (e *ServiceError) WithDetails(v any) (*ServiceError, error)

WithDetails returns a copy of the error with JSON details attached. Returns an error if v cannot be marshaled to JSON.

func (*ServiceError) WithService

func (e *ServiceError) WithService(service string) *ServiceError

WithService returns a copy of the error with the service name set.

Jump to

Keyboard shortcuts

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