Documentation
¶
Overview ¶
Package problem implements the AtomiCloud RFC 9457 problem-details contract (C0 §2/§14) in Go idiom.
Unlike the TypeScript/Dart/C# siblings, the Go family has NO result monad: fallible APIs return the idiomatic (T, error) pair and carry problem metadata on a concrete Error that is compatible with errors.Is/errors.As and %w wrapping. Consumers recover the Problem with `var pe *problem.Error; errors.As(err, &pe)`.
The RFC 9457 `type` URI is minted in exactly ONE place — TypeURI — from an ErrorPortal service-tree block, with the `{version}` segment kept (D8). Every envelope, registry entry, catalog row, and local error resolves its `type` through that single builder, which is the duplication this package exists to prevent.
The public surface mirrors the accepted Dart sibling 1:1 where Go allows:
- Problem — the RFC 9457 envelope plus the `data` and `recoverable` extensions, with lossless JSON round-tripping.
- ErrorPortal / TypeURI / InvalidSegmentError — the single-source type-URI builder and its boundary validation.
- Type / Registry and the GenericProblems baseline set.
- CatalogEntry / CatalogEndpoint / Catalog — the catalog export producing per-row Problem CR content (C0 §14).
- Error and FromObject — the problem-typed error and the total value→Problem transformer.
- LocalError / ErrorSink — unexpected-error wrapping into a `local-error` Problem carrying the message and stack in `data`.
Index ¶
- Constants
- func RegisterGenerics(registry *Registry) error
- func TypeURI(portal ErrorPortal, version, id string) (string, error)
- type Catalog
- func (c *Catalog) Add(entry CatalogEntry)
- func (c *Catalog) AddGenerics() error
- func (c *Catalog) AddType(problemType Type, endpoints ...CatalogEndpoint) error
- func (c *Catalog) Entries() []CatalogEntry
- func (c *Catalog) Lookup(id string) (CatalogEntry, bool)
- func (c *Catalog) Portal() ErrorPortal
- func (c *Catalog) ToCRDContent() []map[string]any
- type CatalogEndpoint
- type CatalogEntry
- type DuplicateTypeError
- type Error
- type ErrorPortal
- type ErrorSink
- type InvalidSegmentError
- type LocalError
- type NoopErrorSink
- type Problem
- type Registry
- type TransformOptions
- type Type
- type UnknownTypeError
Examples ¶
Constants ¶
const UncataloguedProblemID = "uncatalogued"
UncataloguedProblemID is the id carried by an unexpected/uncatalogued problem (C0 §14 uncatalogued ⇒ 5xx ⇒ catalog-loop rule). It is used as the fallback id when no concrete registry entry is known.
Variables ¶
This section is empty.
Functions ¶
func RegisterGenerics ¶
RegisterGenerics registers the GenericProblems baseline set on registry, returning the first DuplicateTypeError encountered.
func TypeURI ¶
func TypeURI(portal ErrorPortal, version, id string) (string, error)
TypeURI builds the RFC 9457 `type` URI from an ErrorPortal block.
This is the single source of the C0 §2 template `{scheme}://{host}/docs/{landscape}/{platform}/{service}/{module}/{version}/{id}`. Every problem — catalog entries, runtime envelopes, local errors — resolves its `type` through this function. Each segment must be a non-empty single path segment; otherwise an InvalidSegmentError is returned.
Example ¶
ExampleTypeURI shows the single-source type-URI builder expanding the C0 §2 template (with the deliberate {version} segment).
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-errors-problems/lib/problem"
)
func examplePortal() problem.ErrorPortal {
return problem.ErrorPortal{
Scheme: "https",
Host: "docs.raichu.cluster.atomi.cloud",
Landscape: "raichu",
Platform: "go",
Service: "user",
Module: "api",
}
}
func main() {
uri, _ := problem.TypeURI(examplePortal(), "v1", "entity-not-found")
fmt.Println(uri)
}
Output: https://docs.raichu.cluster.atomi.cloud/docs/raichu/go/user/api/v1/entity-not-found
Types ¶
type Catalog ¶
type Catalog struct {
// contains filtered or unexported fields
}
Catalog is a per-service×landscape catalog of [CatalogEntry]s. It builds entries from a Type (so the type URI is always the single-source one) and emits the full Problem CR content payload.
Example ¶
ExampleCatalog shows building the C0 §14 Problem CR content for an endpoint that can return a generic problem.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-errors-problems/lib/problem"
)
func examplePortal() problem.ErrorPortal {
return problem.ErrorPortal{
Scheme: "https",
Host: "docs.raichu.cluster.atomi.cloud",
Landscape: "raichu",
Platform: "go",
Service: "user",
Module: "api",
}
}
func main() {
catalog := problem.NewCatalog(examplePortal())
_ = catalog.AddType(problem.EntityNotFound(), problem.CatalogEndpoint{Method: "GET", Path: "/user/{id}"})
content := catalog.ToCRDContent()
fmt.Println(content[0]["id"], content[0]["status"], content[0]["recoverable"])
}
Output: entity-not-found 404 false
func NewCatalog ¶
func NewCatalog(portal ErrorPortal, entries ...CatalogEntry) *Catalog
NewCatalog creates a catalog bound to portal, optionally pre-populated with entries.
func (*Catalog) Add ¶
func (c *Catalog) Add(entry CatalogEntry)
Add stores entry, replacing any existing entry with the same id while keeping its original insertion position.
func (*Catalog) AddGenerics ¶
AddGenerics adds the GenericProblems baseline set, returning the first InvalidSegmentError encountered while building type URIs.
func (*Catalog) AddType ¶
func (c *Catalog) AddType(problemType Type, endpoints ...CatalogEndpoint) error
AddType builds and adds an entry from a registry problemType, attaching endpoints. The type URI is minted by the single-source builder via the catalog's portal, so this is the canonical way to declare a cataloged problem. A default status of 500 is applied when the type has no status hint.
func (*Catalog) Entries ¶
func (c *Catalog) Entries() []CatalogEntry
Entries returns the declared entries in insertion order.
func (*Catalog) Lookup ¶
func (c *Catalog) Lookup(id string) (CatalogEntry, bool)
Lookup returns the entry registered for id and whether it was present.
func (*Catalog) Portal ¶
func (c *Catalog) Portal() ErrorPortal
Portal returns the portal used when building type URIs for entries added via Catalog.AddType.
func (*Catalog) ToCRDContent ¶
ToCRDContent emits the full Problem CR content payload (C0 §14): the `problems[]` list rendered per row by the service's primordial chart.
type CatalogEndpoint ¶
type CatalogEndpoint struct {
// Method is the HTTP method, e.g. GET, POST.
Method string
// Path is the absolute path, e.g. /user/me.
Path string
}
CatalogEndpoint is a method+path pair declaring which endpoint can return a problem.
func (CatalogEndpoint) ToContent ¶
func (e CatalogEndpoint) ToContent() map[string]any
ToContent renders the endpoint as its Problem CR content member.
type CatalogEntry ¶
type CatalogEntry struct {
// ID is the stable problem id.
ID string
// TypeURI is the RFC 9457 `type` URI built by [TypeURI].
TypeURI string
// Title is a short human-readable title.
Title string
// Status is the HTTP status code.
Status int
// Recoverable is the recoverable-vs-fatal flag the frontend classifier reads.
Recoverable bool
// DataSchema is the JSON Schema of the `data` extension payload.
DataSchema map[string]any
// Endpoints are the endpoints that can return this problem.
Endpoints []CatalogEndpoint
}
CatalogEntry is one per-endpoint catalog entry (C0 §14 `problems[]` shape).
The TypeURI is built in exactly ONE place; callers pass a URI minted by TypeURI (typically via Registry.TypeURIFor or Catalog.AddType) so no second template exists.
func (CatalogEntry) ToCRDContent ¶
func (e CatalogEntry) ToCRDContent() map[string]any
ToCRDContent renders the entry as its Problem CR content member (C0 §14): `{id, type, title, status, recoverable, data, endpoints[]}`.
type DuplicateTypeError ¶
type DuplicateTypeError struct {
// ID is the duplicate problem id.
ID string
}
DuplicateTypeError reports an attempt to register a problem id that is already present in a Registry.
func (*DuplicateTypeError) Error ¶
func (e *DuplicateTypeError) Error() string
Error implements the error interface.
type Error ¶
type Error struct {
// Problem is the RFC 9457 envelope this error carries.
Problem Problem
// contains filtered or unexported fields
}
Error is the Go realization of the family "result slot": a concrete error that carries a Problem and is compatible with errors.Is/errors.As and %w wrapping. Fallible APIs return the idiomatic (T, error) pair; consumers recover the envelope with:
var pe *problem.Error
if errors.As(err, &pe) {
use(pe.Problem)
}
Example ¶
ExampleError shows recovering a Problem from a wrapped (T, error) result via errors.As — the Go realization of the family result slot.
package main
import (
"errors"
"fmt"
"github.com/AtomiCloud/diene.go-errors-problems/lib/problem"
)
func main() {
envelope := problem.Problem{Type: "https://x/v1/conflict", Title: "Conflict", Status: 409}
err := fmt.Errorf("saving user: %w", problem.NewError(envelope))
var problemErr *problem.Error
if errors.As(err, &problemErr) {
fmt.Println(problemErr.Problem.Status)
}
}
Output: 409
func WrapError ¶
WrapError creates a problem-typed error carrying problem and wrapping cause, so errors.Is/errors.As traverse into the underlying error chain.
type ErrorPortal ¶
type ErrorPortal struct {
// Scheme is the URI scheme, conventionally `https`.
Scheme string
// Host serves the problem docs, e.g. docs.raichu.cluster.atomi.cloud.
Host string
// Landscape is the LPSM `L` segment.
Landscape string
// Platform is the LPSM `P` segment.
Platform string
// Service is the LPSM `S` segment.
Service string
// Module is the LPSM `M` segment.
Module string
}
ErrorPortal is the service-tree config block the problem type-URI template is fed from (C0 §2). The LPSM segments (Landscape/Platform/Service/Module) are the declaring identity that also derives public hostnames, so a type URI is stable, addressable, and never hand-authored per row.
Real services supply their build-time portal (sourced from config, never hardcoded per R4); LocalErrorPortal is the fallback for client-local errors that have no backend context.
func LocalErrorPortal ¶
func LocalErrorPortal() ErrorPortal
LocalErrorPortal is the default portal for client-local errors with no service backend context (e.g. a crash before any backend is known). Real programs pass their build-time LPSM portal instead; this keeps the single builder usable in isolation and in tests.
type ErrorSink ¶
type ErrorSink interface {
// Capture reports problem, returning any delivery error.
Capture(ctx context.Context, problem Problem) error
}
ErrorSink receives unexpected problems captured by LocalError. In production this forwards to the telemetry path; tests inject a recording sink.
type InvalidSegmentError ¶
type InvalidSegmentError struct {
// Name is the offending segment name (e.g. "host", "id").
Name string
// Value is the rejected segment value.
Value string
}
InvalidSegmentError reports a problem type-URI segment that is empty or contains a `/`. Validating segments at the boundary keeps the URI canonical and catches misconfigured portals instead of producing a malformed URI.
Example ¶
ExampleInvalidSegmentError shows the boundary validation rejecting a segment that contains a slash.
package main
import (
"errors"
"fmt"
"github.com/AtomiCloud/diene.go-errors-problems/lib/problem"
)
func main() {
_, err := problem.TypeURI(problem.LocalErrorPortal(), "v1", "bad/id")
var segmentErr *problem.InvalidSegmentError
if errors.As(err, &segmentErr) {
fmt.Println(segmentErr.Name)
}
}
Output: id
func (*InvalidSegmentError) Error ¶
func (e *InvalidSegmentError) Error() string
Error implements the error interface.
type LocalError ¶
type LocalError struct {
// Sink is the destination for captured local-error problems.
Sink ErrorSink
// Portal builds the local-error type URI.
Portal ErrorPortal
}
LocalError wraps unexpected errors into a `local-error` Problem carrying the `message` and `stackTrace` in `data`, and captures it on a ErrorSink. The type URI flows through the single-source builder so even local errors share one identity shape.
Example ¶
ExampleLocalError shows wrapping an unexpected error into a local-error Problem carrying the message and stack in data.
package main
import (
"context"
"errors"
"fmt"
"github.com/AtomiCloud/diene.go-errors-problems/lib/problem"
)
func main() {
local := problem.NewLocalError(problem.NoopErrorSink{})
envelope, _ := local.Wrap(context.Background(), errors.New("boom"), "goroutine 1 [running]")
fmt.Println(envelope.Type)
fmt.Println(envelope.Data["message"])
}
Output: https://local.atomi.cloud/docs/local/go/app/core/v1/local-error boom
func NewLocalError ¶
func NewLocalError(sink ErrorSink) LocalError
NewLocalError creates a wrapper writing captures to sink, using LocalErrorPortal for its type URIs.
func NewLocalErrorWithPortal ¶
func NewLocalErrorWithPortal(sink ErrorSink, portal ErrorPortal) LocalError
NewLocalErrorWithPortal creates a wrapper writing captures to sink and building type URIs from portal.
type NoopErrorSink ¶
type NoopErrorSink struct{}
NoopErrorSink is an ErrorSink that discards every capture.
type Problem ¶
type Problem struct {
// Type is the RFC 9457 `type` URI identifying the problem type.
Type string
// Title is the RFC 9457 short, human-readable summary.
Title string
// Status is the RFC 9457 origin-generated HTTP status code.
Status int
// Detail is the RFC 9457 human-readable, occurrence-specific explanation.
// It is optional: nil is omitted from the wire form, a non-nil pointer
// (including a pointer to an empty string) is emitted.
Detail *string
// Instance is the RFC 9457 URI identifying the specific occurrence. It is
// optional with the same nil-omitted / present-emitted semantics as Detail.
Instance *string
// Recoverable reports whether the frontend may offer a retry (C0 §2/§14).
Recoverable bool
// Data is the typed payload extension (schema published per problem).
Data map[string]any
}
Problem is an RFC 9457 problem-details envelope plus the AtomiCloud `data` and `recoverable` extensions (C0 §2).
The standard RFC 9457 members are Type, Title, Status, Detail, and Instance. The extensions are Data (the typed payload whose schema is published per problem in the catalog) and Recoverable (the flag the frontend classifier splits retry-vs-fatal on). Detail and Instance are optional Go `*string` values mirroring RFC 9457's (and the Dart sibling's) nullable members: a nil pointer is absent and omitted from the wire form, while a non-nil pointer — including a pointer to an empty string — is present and emitted. This keeps a wire envelope with `"detail":""`/`"instance":""` losslessly round-trippable.
Type is always a URI minted by TypeURI; this type never formats the template itself.
Example ¶
ExampleProblem shows the RFC 9457 envelope marshalling to its canonical wire shape (keys are emitted in encoding/json's sorted order).
package main
import (
"encoding/json"
"fmt"
"github.com/AtomiCloud/diene.go-errors-problems/lib/problem"
)
func main() {
envelope := problem.Problem{
Type: "https://docs.example/v1/entity-not-found",
Title: "Entity not found",
Status: 404,
Recoverable: false,
Data: map[string]any{"resource": "user"},
}
data, _ := json.Marshal(envelope)
fmt.Println(string(data))
}
Output: {"data":{"resource":"user"},"recoverable":false,"status":404,"title":"Entity not found","type":"https://docs.example/v1/entity-not-found"}
func FromObject ¶
func FromObject(value any, options TransformOptions) Problem
FromObject folds value into a typed Problem. It never panics: the whole point is to guarantee a Problem for any value.
- A Problem value is returned unchanged.
- An error carrying a Error anywhere in its chain (via errors.As) yields that error's Problem.
- When options.Registry recognises a `problemId` carried on a map[string]any value, the registry's type/title/status/recoverable are used and the value's `data` map is preserved.
- Otherwise an uncatalogued problem (C0 §14) is produced with the UncataloguedProblemID fallback type.
Example ¶
ExampleFromObject shows the total value→Problem fold producing an uncatalogued 5xx problem for an unrecognised error (C0 §14).
package main
import (
"errors"
"fmt"
"github.com/AtomiCloud/diene.go-errors-problems/lib/problem"
)
func main() {
envelope := problem.FromObject(errors.New("disk full"), problem.DefaultTransformOptions())
fmt.Println(envelope.Status, *envelope.Detail)
}
Output: 500 disk full
func (Problem) Equal ¶
Equal reports whether p and other serialize to the same canonical JSON. It is value equality tolerant of int-vs-float numeric decoding across the wire.
func (Problem) MarshalJSON ¶
MarshalJSON renders the envelope in its canonical wire shape (`type,title,status,detail?,instance?,recoverable,data`). Detail and Instance are omitted when nil and emitted (even for an empty string) when non-nil; Data always renders as an object (never null).
func (Problem) String ¶
String returns a compact human-readable form of the envelope.
Example ¶
ExampleProblem_String shows the compact human-readable form.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-errors-problems/lib/problem"
)
func main() {
envelope := problem.Problem{Type: "https://x/v1/conflict", Title: "Conflict", Status: 409}
fmt.Println(envelope.String())
}
Output: Problem(https://x/v1/conflict, 409, Conflict)
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is an enumerable registry of [Type]s bound to an ErrorPortal. The portal supplies the LPSM segments every type URI is built from, so a registry is the per-service×landscape source of truth the catalog emitter renders into Problem CR content (C0 §14).
Example ¶
ExampleRegistry shows registering the generic baseline and resolving a type URI through the single-source builder.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-errors-problems/lib/problem"
)
func examplePortal() problem.ErrorPortal {
return problem.ErrorPortal{
Scheme: "https",
Host: "docs.raichu.cluster.atomi.cloud",
Landscape: "raichu",
Platform: "go",
Service: "user",
Module: "api",
}
}
func main() {
registry, _ := problem.NewRegistry(examplePortal())
_ = problem.RegisterGenerics(registry)
entityNotFound, _ := registry.Require("entity-not-found")
uri, _ := registry.TypeURIFor(entityNotFound)
fmt.Println(uri)
}
Output: https://docs.raichu.cluster.atomi.cloud/docs/raichu/go/user/api/v1/entity-not-found
func NewRegistry ¶
func NewRegistry(portal ErrorPortal, types ...Type) (*Registry, error)
NewRegistry creates a registry bound to portal, optionally pre-populated with types. It returns a DuplicateTypeError if types contains a repeated id.
func (*Registry) Portal ¶
func (r *Registry) Portal() ErrorPortal
Portal returns the error-portal block every type URI is built from.
func (*Registry) Require ¶
Require returns the type registered for id, or an UnknownTypeError when it is absent.
type TransformOptions ¶
type TransformOptions struct {
// Portal builds fallback type URIs for uncatalogued problems.
Portal ErrorPortal
// Registry, when non-nil, is consulted for values carrying a known
// `problemId`.
Registry *Registry
// DefaultStatus is the status for an uncatalogued fallback problem.
DefaultStatus int
// DefaultVersion is the version segment for fallback type URIs.
DefaultVersion string
}
TransformOptions configures how FromObject folds an arbitrary value into a Problem.
func DefaultTransformOptions ¶
func DefaultTransformOptions() TransformOptions
DefaultTransformOptions returns options that mint fallback URIs from LocalErrorPortal with a 500 status and the v1 version segment, and no registry.
type Type ¶
type Type struct {
// ID is the stable identifier, e.g. "entity-not-found".
ID string
// Title is a short human-readable title.
Title string
// Version is the contract version segment, e.g. "v1".
Version string
// Status is the default HTTP status hint (0 means "no default").
Status int
// Recoverable reports whether the frontend may offer a retry (C0 §2/§14).
Recoverable bool
// DataSchema is the JSON Schema describing the `data` extension payload.
DataSchema map[string]any
}
Type is a versioned problem-type declaration. Version is part of the contract identity (C0 §2): bumping it mints a NEW problem type URI rather than mutating an existing one.
func Conflict ¶
func Conflict() Type
Conflict is the generic 409 problem: a state conflict (duplicate, stale version, …). It is recoverable.
func EntityNotFound ¶
func EntityNotFound() Type
EntityNotFound is the generic 404 problem: the referenced entity does not exist. It is not recoverable.
func GenericProblems ¶
func GenericProblems() []Type
GenericProblems returns the portable v1 baseline catalog in stable order: validation-error, entity-not-found, conflict, unauthenticated, unauthorized, invalid-json. Domain problems stay in consumer services and are never part of this set.
Example ¶
ExampleGenericProblems lists the portable v1 baseline catalog in stable order.
package main
import (
"fmt"
"github.com/AtomiCloud/diene.go-errors-problems/lib/problem"
)
func main() {
for _, problemType := range problem.GenericProblems() {
fmt.Println(problemType.ID)
}
}
Output: validation-error entity-not-found conflict unauthenticated unauthorized invalid-json
func InvalidJSON ¶
func InvalidJSON() Type
InvalidJSON is the generic 400 problem: the request body was not valid JSON. It is recoverable.
func Unauthenticated ¶
func Unauthenticated() Type
Unauthenticated is the generic 401 problem: the authenticated identity is missing or invalid. It is recoverable.
func Unauthorized ¶
func Unauthorized() Type
Unauthorized is the generic 403 problem: the identity is authenticated but lacks permission. It is not recoverable.
func ValidationError ¶
func ValidationError() Type
ValidationError is the generic 400 problem: the request body failed validation. It is recoverable and carries a `fields[]` payload.
type UnknownTypeError ¶
type UnknownTypeError struct {
// ID is the missing problem id.
ID string
}
UnknownTypeError reports a lookup for a problem id that is not registered.
func (*UnknownTypeError) Error ¶
func (e *UnknownTypeError) Error() string
Error implements the error interface.