shared

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package shared holds the vocabulary every bounded context needs: identifiers, locales, and the error model.

Errors are typed values, never strings (arc42 §8.11). A caller decides on the type, not by matching on a message - a message is display text, and display text does not exist in the backend (ADR-0011). What travels instead is a stable code plus parameters; the client turns that into a sentence in its own language.

Index

Constants

View Source
const MaxLanguageTagLength = 35

MaxLanguageTagLength is the bound a well-formed tag stays inside. Long enough for the longest tag anybody writes - `zh-Hant-HK-x-private` and its like - and short enough that a field holding one cannot carry a paragraph.

Variables

View Source
var (
	ErrValidation       = New(CategoryValidation, "validation_failed")
	ErrNotFound         = New(CategoryNotFound, "not_found")
	ErrConflict         = New(CategoryConflict, "conflict")
	ErrVersionConflict  = New(CategoryConflict, "version_conflict")
	ErrForbidden        = New(CategoryForbidden, "forbidden")
	ErrUnauthenticated  = New(CategoryUnauthenticated, "unauthenticated")
	ErrGone             = New(CategoryGone, "gone")
	ErrRateLimited      = New(CategoryRateLimited, "rate_limited")
	ErrUnavailable      = New(CategoryUnavailable, "dependency_unavailable")
	ErrMalformedRequest = New(CategoryValidation, "malformed_request")
	ErrInternal         = New(CategoryInternal, "internal")
	// ErrCapabilityNotSupported is a field set on an item type whose profile does not carry the
	// capability (ADR-0006, domain-model.md §2). Its own code rather than a validation_failed,
	// because the answer to it is different: the request is well formed and the field is real,
	// and what a client has to change is the type it is writing to - which is worth telling it in
	// the code rather than only in a field error.
	ErrCapabilityNotSupported = New(CategoryValidation, "capability_not_supported")
)

The contract codes from api-guidelines.md §6. They are sentinels and constructors at once: comparable with errors.Is, and a starting point for WithParams/WithDetail.

LabelTokens lists every label colour, in the order the design system declares them. A client that offers a colour picker gets the order from here rather than sorting the names.

Functions

func IsLabelToken

func IsLabelToken(name string) bool

IsLabelToken reports whether a name is one of the label colours.

The check is a membership test over a generated list rather than a hand-written switch, so that an eleventh colour added to tokens.json cannot be accepted by the frontend and refused here.

func LanguageTag

func LanguageTag(raw string) (string, bool)

LanguageTag checks the shape of a BCP 47 tag - `de`, `de-AT`, `pt-BR`, `zh-Hans` - and answers it trimmed. An empty string is well formed and means "not stated"; anything else that is not a tag is refused.

Structural on purpose, in both places that use it. Which locales this product has translations for is a catalogue (i18n-l10n.md §2), and which languages it can *index* is what the installation's PostgreSQL has (ADR-0034) - neither of those is a reason to refuse a tag. A language with no translation falls back, and one with no text search configuration is indexed word by word. Both are answers; a validation error would not be.

It lives here rather than in one of the two contexts because both need exactly this and neither owns it: an account's locale is identity's, an entry's content language is work's, and the grammar of a tag is neither's (Errors.go, "the vocabulary every bounded context needs").

func NFC

func NFC(raw string, form text.Normalizer) (string, error)

NFC brings the input to normal form C through the port (i18n-l10n.md §5), and does nothing else: whether a text is trimmed is each kind's own rule - a title is, a comment is prose and is stored as sent - and this is the one thing every kind shares.

Every constructor that stores user text calls this before it bounds and checks the value, so that the length is counted, the uniqueness index compares, and the search document indexes the one spelling a person sees. It lives here rather than in each bounded context for the reason LanguageTag does: a title is work's, a display name is identity's, and the normal form of text is neither's.

Without a port, text that is not ASCII is refused rather than stored as it arrived - fail closed, exactly as an email's domain is without its encoder: a caller that forgot to wire the port cannot create the second label this exists to prevent. ASCII cannot be in any other form than its own, so a caller that never sees anything else loses nothing. The refusal is an internal error and not a validation one: nothing the client sent is wrong.

Types

type ActorKind

type ActorKind string

ActorKind is who is acting: the vocabulary of audit.md §2, and the one an event, an audit entry and a request context all speak.

It lives here rather than in the layer that first needed it, because three copies of the same five words is how an audit trail comes to record `AI_AGENT` while an event says `agent`. A port may import this package and nothing else of the domain (project-structure.md §2), which is exactly what a shared vocabulary has to satisfy.

const (
	// ActorAnonymous is a request that carried no credential. It never reaches an audit entry as
	// an actor - an unauthenticated request performs no auditable action - but a refused one is
	// recorded with outcome DENIED, and it needs a name for that.
	ActorAnonymous      ActorKind = "ANONYMOUS"
	ActorUser           ActorKind = "USER"
	ActorServiceAccount ActorKind = "SERVICE_ACCOUNT"
	ActorAutomation     ActorKind = "AUTOMATION"
	ActorAIAgent        ActorKind = "AI_AGENT"
	// ActorSystem is the installation acting for itself: a scheduled job, a retention run, a
	// migration.
	ActorSystem ActorKind = "SYSTEM"
)

func ActorKinds

func ActorKinds() []ActorKind

ActorKinds returns every defined kind.

func (ActorKind) Auditable

func (k ActorKind) Auditable() bool

Auditable reports whether the kind may appear as the actor of an audit entry. The database constrains the same five values (db/schema.sql, audit_log.actor_type), so an entry written with ANONYMOUS would be refused at the boundary - this is what refuses it before the transaction.

func (ActorKind) Valid

func (k ActorKind) Valid() bool

Valid reports whether the kind is one of the defined ones.

type Category

type Category string

Category is the coarse classification the adapters map to a protocol. The application layer assigns it; an adapter never invents one.

arc42 §8.11 names all nine and the status each maps to; api-guidelines.md §6 is the mapping UNAUTHENTICATED, GONE and UNAVAILABLE exist for, because 401, 410 and 503 are statuses none of the other six can produce.

const (
	// CategoryValidation is input the domain rejects: a malformed field, a broken invariant,
	// a capability the item type does not have.
	CategoryValidation Category = "VALIDATION"
	// CategoryNotFound is a resource that does not exist, or that the actor may not know exists.
	CategoryNotFound Category = "NOT_FOUND"
	// CategoryConflict is a clash with the current state: a duplicate, a stale version, a cycle
	// in the hierarchy.
	CategoryConflict Category = "CONFLICT"
	// CategoryForbidden is an authenticated actor without the permission for this operation.
	CategoryForbidden Category = "FORBIDDEN"
	// CategoryUnauthenticated is a missing, expired, or unreadable credential.
	CategoryUnauthenticated Category = "UNAUTHENTICATED"
	// CategoryGone is a resource that existed and was permanently deleted. Distinct from
	// NOT_FOUND, because the difference is the whole point for a synchronising client.
	CategoryGone Category = "GONE"
	// CategoryRateLimited is a limit reached: per IP, per token, per tenant.
	CategoryRateLimited Category = "RATE_LIMITED"
	// CategoryUnavailable is a dependency that is unreachable or deliberately degraded. It says
	// "later", not "wrong" - the client may retry (ADR-0016).
	CategoryUnavailable Category = "UNAVAILABLE"
	// CategoryInternal is a defect. Everything unclassified lands here, and nothing about it
	// reaches the client beyond the code and the request ID (security.md §9).
	CategoryInternal Category = "INTERNAL"
)

func Categories

func Categories() []Category

Categories returns every defined category. An adapter uses it to prove it handles all of them - the observability layer derives its `result` label from this set, so a new category shows up in the metrics by itself instead of being silently folded into INTERNAL (observability-reliability.md §4.1).

func (Category) Valid

func (c Category) Valid() bool

Valid reports whether the category is one of the defined ones. An adapter uses this to decide on a status; an unknown category must be treated as INTERNAL rather than silently passed on.

type DataClass

type DataClass string

DataClass is what a piece of data *is*, in the six classes `data-protection.md` §3 names and ADR-0018 decision 1 makes a property of the model rather than a document.

One vocabulary, here. The three levels `audit.md` §4 names are not classes at all but a *masking* policy derived from them. A gate cannot reconcile several vocabularies; it can reconcile one and a derivation, which is what this type and `audit.MaskingFor` are.

const (
	// ClassNonPersonal is configuration, enum values, counters. No restriction.
	ClassNonPersonal DataClass = "NON_PERSONAL"
	// ClassPersonalBasic is a name, an address, an avatar, a locale: a person's identity rather
	// than their work.
	ClassPersonalBasic DataClass = "PERSONAL_BASIC"
	// ClassPersonalContent is what somebody wrote: titles, notes, comments, attachments, the
	// history of an entry.
	ClassPersonalContent DataClass = "PERSONAL_CONTENT"
	// ClassPersonalTechnical is an address, a user agent, a device characteristic - stored
	// truncated and kept briefly.
	ClassPersonalTechnical DataClass = "PERSONAL_TECHNICAL"
	// ClassSpecialCategoryRisk is the class the product does not collect and cannot prevent: a
	// task manager holds no health data, and "reschedule MRI appointment, oncology" sits in a free
	// text field (Art. 9). Naming it is what makes the care around free text deliberate.
	ClassSpecialCategoryRisk DataClass = "SPECIAL_CATEGORY_RISK"
	// ClassSecret is a password, a token, a key. Only hashed or encrypted, never exported, never
	// audited.
	ClassSecret DataClass = "SECRET"
)

func DataClasses

func DataClasses() []DataClass

DataClasses returns every class, in the order §3's table lists them.

func (DataClass) Personal

func (c DataClass) Personal() bool

Personal reports whether the class describes a person. It is the question every gate over the data catalogue asks: `NON_PERSONAL` needs no deletion path, and everything else does.

func (DataClass) Valid

func (c DataClass) Valid() bool

Valid reports whether the class is one of the six.

type Error

type Error struct {
	Category Category
	// Code is the coarse, stable contract code: validation_failed, not_found, conflict.
	Code string
	// DetailCode names the concrete case: items.cover_not_supported_for_type. Optional; without
	// it the client falls back to Code.
	DetailCode string
	// Params carry the values of the message, never a finished sentence (ADR-0011).
	Params map[string]string
	// Fields lists per-field findings for a validation error.
	Fields []FieldError
	// contains filtered or unexported fields
}

Error is the error type of the domain and the application layer.

Code is part of the API contract: stable, machine-readable, and removing one is a breaking change (versioning-release.md §2). DetailCode plus Params are what the client localises - see locales/en.json.

func AsError

func AsError(err error) *Error

AsError classifies any error for an adapter. An error that is not of this type is a defect and becomes INTERNAL - deliberately without its text, because the text of an unknown error may contain anything, including a connection string (security.md §9).

func Internalf

func Internalf(format string, args ...any) *Error

Internalf builds an internal error from a technical cause. The format string is for the log; nothing of it reaches the client.

func New

func New(category Category, code string) *Error

New builds an error of the given category. Prefer one of the constructors below - they use the codes from the contract in api-guidelines.md §6.

func Validation

func Validation(path, code string, params map[string]string) *Error

Validation builds a validation error for one field.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface. The text is for the log, not for a user - which is why it consists of codes rather than prose.

func (*Error) Is

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

Is makes two errors of this type comparable by their code, so that a caller can write errors.Is(err, shared.ErrNotFound) without knowing the concrete instance.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes the technical cause to errors.Is and errors.As. It is deliberately not part of what an adapter renders.

func (*Error) WithCause

func (e *Error) WithCause(cause error) *Error

WithCause returns a copy carrying the technical origin for the log.

func (*Error) WithDetail

func (e *Error) WithDetail(detailCode string) *Error

WithDetail returns a copy with the concrete case named.

func (*Error) WithFields

func (e *Error) WithFields(fields ...FieldError) *Error

WithFields returns a copy carrying per-field findings.

func (*Error) WithParams

func (e *Error) WithParams(params map[string]string) *Error

WithParams returns a copy carrying the given parameters. The receiver stays untouched, so that package-level sentinels cannot be modified by a caller.

type FieldError

type FieldError struct {
	Path   string
	Code   string
	Params map[string]string
}

FieldError points at one field of the request. Path is a JSON Pointer (RFC 6901), so that a client can highlight the input that is wrong.

type HLC

type HLC struct {
	// Physical is truncated to milliseconds. Finer resolution would be a promise the counter is
	// there to keep instead, and it would differ between platforms.
	Physical time.Time
	Counter  uint32
	// Device is the origin: a client device, or the installation itself for a server-side change.
	Device string
}

HLC is a hybrid logical clock: physical time, a counter, and the device that stamped it (offline-sync.md §4.1).

Device clocks are wrong, sometimes by hours. "The latest timestamp wins" therefore lets a device with a fast clock outvote every other one permanently, which is why every field change carries one of these instead of a bare timestamp: the counter orders changes made within the same millisecond, and the device identifier breaks the remaining ties the same way on every device, so two clients merging the same pair of changes reach the same result.

The textual form is `<physical>:<counter>:<device>`, with the first two zero-padded so that comparing two of them as strings compares them as clocks. The database stores it as text (db/schema.sql, change_log.hlc), and an index on that column then sorts in clock order.

func NewHLC

func NewHLC(physical time.Time, counter uint32, device string) (HLC, error)

NewHLC builds a clock reading. It is the only way to get one with a device that cannot be parsed back.

func ParseHLC

func ParseHLC(raw string) (HLC, error)

ParseHLC reads the textual form. It is what a client's mutation arrives as, so an unusable value is input rather than a defect - the caller decides what to do with a client that sends nonsense (offline-sync.md §3.2).

func (HLC) After

func (h HLC) After(other HLC) bool

After reports whether this reading wins a last-writer-wins comparison against the other one.

func (HLC) Compare

func (h HLC) Compare(other HLC) int

Compare orders two readings: physical time first, then the counter, then the device.

The device is part of the order rather than a tie-break left to chance. Two devices that stamped a change in the same millisecond with the same counter have to be ordered the same way everywhere, or the two of them converge on different results - which is the one failure a merge rule must not have.

func (HLC) IsZero

func (h HLC) IsZero() bool

IsZero reports the absent reading.

func (HLC) String

func (h HLC) String() string

String is the stored and transmitted form.

func (HLC) Tick

func (h HLC) Tick(now time.Time, device string) (HLC, error)

Tick returns the next reading of this clock for the given wall time.

The physical part only ever moves forward: a wall clock that jumped backwards - an NTP correction, a suspended laptop - would otherwise produce a change that sorts before one already recorded, and the change would be silently discarded on merge. When time has not moved on, the counter does; when the counter is exhausted, the clock borrows a millisecond from the future, which is the standard way out and is bounded by how fast a device can produce changes.

type ID

type ID string

ID identifies anything the system stores. The values are UUIDv7, generated in the application through the IDGenerator port rather than by the database (db/schema.sql conventions, arc42 §8.13): time-ordered, so an index stays dense, and known before the insert, so an event can reference the row it describes.

A distinct type rather than a string, because a tenant ID and an item ID are both 36 characters and nothing but the type stops one being passed where the other belongs.

func MustParseID

func MustParseID(raw string) ID

MustParseID is for constants and test fixtures, where a malformed literal is a programming error rather than input.

func ParseID

func ParseID(raw string) (ID, error)

ParseID accepts the canonical form 8-4-4-4-12 with lower-case hex digits. Upper case is rejected rather than folded: two spellings of one identifier turn into two cache keys, two index entries and one hard afternoon.

func (ID) IsUUIDv7

func (id ID) IsUUIDv7() bool

IsUUIDv7 reports whether the version nibble says 7. Not enforced by ParseID: identifiers from an import or an older installation may carry another version, and rejecting them at the door would make a migration impossible.

func (ID) IsZero

func (id ID) IsZero() bool

IsZero reports the absent identifier. Used for optional references - an actor is absent when the system itself acts.

func (ID) String

func (id ID) String() string

type LabelToken

type LabelToken string

LabelToken is the name of a label colour. The domain stores the name, never the colour: what a token looks like is a question for the theme the client is rendering in, and answering it here would put display information in the backend (ADR-0011, ADR-0029).

const (
	LabelTokenSlate   LabelToken = "slate"
	LabelTokenBlue    LabelToken = "blue"
	LabelTokenTeal    LabelToken = "teal"
	LabelTokenGreen   LabelToken = "green"
	LabelTokenLime    LabelToken = "lime"
	LabelTokenAmber   LabelToken = "amber"
	LabelTokenOrange  LabelToken = "orange"
	LabelTokenRed     LabelToken = "red"
	LabelTokenMagenta LabelToken = "magenta"
	LabelTokenViolet  LabelToken = "violet"
)

The 10 label colours the design system defines.

Jump to

Keyboard shortcuts

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