gold

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Aug 31, 2025 License: MIT Imports: 6 Imported by: 0

README

Type-safe Linked Data identities for Golang

Linked Data applications require careful handling of identifiers (IRIs, URNs) and relationships between entities. Manual string manipulation is error-prone and lacks compile-time guarantees. The gold library solves this by providing type-safe Linked Data identities that leverage Go's type system to prevent schema mismatches and ensure correct identifier usage at compile time.

go get github.com/fogfish/gold

Quick Start

package main

import (
    "fmt"
    "github.com/fogfish/gold"
)

// Define domain types
type User struct {
    ID gold.IRI[User] `json:"id"`
}

type Blog struct {
    ID      gold.IRI[Blog] `json:"id"`
    Creator gold.IRI[User] `json:"creator"`
}

func main() {
    // Create type-safe IRIs - schema is derived from type
    userID := gold.ToIRI[User]("alice")     // "user:alice"
    blogID := gold.ToIRI[Blog]("my-blog")   // "blog:my-blog"
    
    user := User{ID: userID}
    blog := Blog{ID: blogID, Creator: userID}
    
    // Compile-time safety: this would fail at compile time
    // blog.Creator = blogID  // ❌ Cannot assign blog ID to user field
}

Core Concepts

Type-Safe IRIs

IRIs are structured as {schema}:{id} where the schema is automatically derived from the Go type name:

type Person struct{}

personID := gold.ToIRI[Person]("john")             // "person:john"
validID, err := gold.AsIRI[Person]("person:jane")  // ✅ Valid
invalidID, err := gold.AsIRI[Person]("user:bob")   // ❌ Error: wrong schema
Hierarchical URNs

For hierarchical identifiers with namespace support:

type MyApp struct{}
type Document struct{}

// Creates "urn:myapp:document:folder:subfolder:file"
urn := gold.ToURN[MyApp, Document]("folder", "subfolder", "file")
iri := urn.ToIRI()  // "document:folder/subfolder/file"
Relationship Modeling

Model relationships between entities using compound types:

// Relationship between User and Blog for compound database keys
type UserBlog = gold.Imply[User, Blog]

relationship := gold.ImplyFrom(
    gold.ToIRI[User]("alice"),
    gold.ToIRI[Blog]("tech-blog")
)
Schema Registry

Schemas are derived automatically, but can be customized:

// For complex types, register custom schemas
gold.Register[[]MyType]("my-type-collection")
Type-Safe URL Templates

URL templates provide compile-time safety for building URLs with typed parameters:

type AccountID = gold.IRI[Account]
type StoryID = gold.IRI[Story]

// Define type-safe URL template
type StoryUrl = gold.Url2[string, AccountID, StoryID]

// Create URL template constants
const CatalogStoryUrl = StoryUrl("/catalog/authors/%s/stories/%s")

// Build URLs with type safety
accountID := gold.ToIRI[Account]("alice")
storyID := gold.ToIRI[Story]("my-story")
url := CatalogStoryUrl.Build(accountID, storyID)
// Result: "/catalog/authors/account:alice/stories/story:my-story"

The library provides Url1 through Url4 for templates with 1-4 parameters respectively.

License

See LICENSE file for details.

Documentation

Overview

Package gold provides a typesafe Linked Data support for Golang. It defines algebra for declaring identity and hierarchical relationships between data types.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DecodeJSON

func DecodeJSON(b []byte) (string, error)

func EncodeJSON

func EncodeJSON(s string, err error) ([]byte, error)

func Register

func Register[T any](schema string) string

func Schema

func Schema[T any]() string

Schema returns a schema for the type T is derived from the type name or type registry. It is recommended to use pure types rather than containers. For any complex types use registry:

gold.Register[[]MyClass]("seq")
gold.Schema([]MyClass)()

Types

type Codec

type Codec[T any] interface {
	Encoder[T]
	Decoder[T]
}

func HashKey

func HashKey[T, A, B any]() Codec[T]

func N3

func N3[T, A, B any]() Codec[T]

type Decoder

type Decoder[T any] interface {
	Decode(string, *T) error
}

type Encoder

type Encoder[T any] interface {
	Encode(*T) (string, error)
}

type IRI

type IRI[T any] string

Compact IRI (CURIE) is universal identifier for a resource, defined as

{schema}:{id}

where schema is a namespace prefix and id is a local identifier. The schema is always derived from the type of the resource T, making it safe at compiletime.

It is recommended to derive schema from the class itself:

type MyClass struct {
  ID gold.IRI[MyClass] `json:"id"`
}

IRIs are not hierarchical in the linked-data sense. They are flat, typed identifiers structured as {schema}:{id}.

func AsIRI

func AsIRI[T any](iri string) (IRI[T], error)

IRI parser, converts a string into a compact IRI type. Input string is expected to be a compact IRI of the form {schema}:{id}. It returns an error if the schema prefix is different than one associated with type T.

func ToIRI

func ToIRI[T any](id string) IRI[T]

IRI type constructor, creates a new IRI from the string. It returns a valid compact IRI annotated with a schema.

If input string is compact IRI of other kind, it is still annotated with a schema.

func (IRI[T]) IsEmpty

func (iri IRI[T]) IsEmpty() bool

Check IRI is defined.

func (*IRI[T]) IsValid

func (iri *IRI[T]) IsValid() bool

Validate IRI to be a valid compact IRI type.

func (IRI[T]) MarshalJSON

func (iri IRI[T]) MarshalJSON() ([]byte, error)

Encodes IRI into JSON as a CURIE string.

func (IRI[T]) Norm

func (iri IRI[T]) Norm() IRI[T]

Normalize IRI to a compact IRI type. Use it as constructor for IRI types aliases.

type MyID = gold.IRI[MyClass]
var id = MyID("foo").Norm()

func (IRI[T]) Reference

func (iri IRI[T]) Reference() string

Reference returns a local identifier of the IRI, without schema prefix.

func (*IRI[T]) UnmarshalJSON

func (iri *IRI[T]) UnmarshalJSON(b []byte) (err error)

Decodes IRI from JSON as a CURIE string.

func (*IRI[T]) Validate

func (iri *IRI[T]) Validate() (err error)

Validate IRI to be a valid compact IRI type, return an error if it is not.

type Imply

type Imply[A, B any] struct {
	Domain IRI[A] `json:"domain"`
	Range  IRI[B] `json:"range"`
}

IRIs are not hierarchical in the linked-data sense. They are flat, typed identifiers structured as {schema}:{id}. IRI assumes hierarchical identifiers but they are not hierarchical in this context {id} is a "flat" identity of the class. Conveniently, it is possible to use IRIs as hierarchical identifiers but it is not reflected in the type system and should be managed by the application itself.

Relations between concepts are not implied by IRI structure itself. Instead, They are modeled explicitly as product of IRIs or using predicate statements:

image:Selfie schema:creator user:Alice

Application models predicate statements as attributes of the IRI type

type Image struct {
  ID      gold.IRI[Image] `json:"id"`
  Creator gold.IRI[User]  `json:"creator"`
}

Statements are not flexible enough to model convolution of statements into composite keys, which are essential for persisting linked-data.

Generic Imply type is used to explicitly model relationships between two types A and B as composite structure.

user:Alice/schema:creator/image:Selfie

type Creator gold.Imply[Image, User]

func ImplyFrom

func ImplyFrom[A, B any](a IRI[A], b IRI[B]) Imply[A, B]

Create Imply statement from two IRIs.

type URN

type URN[N, T any] string

Uniform Resource Name (URN) is a type-safe identifier for hierarchical identity, it is defined as

urn:{namespace}:{schema}:{id⁰}:{id¹}:...:{idⁿ}

where namespace is a unique identifier for the application, schema is a type-safe identifier for the resource, and id is a local identifier.

URNs supports convenient hierarchical identity. It is not reflected in the type system and should be managed by the application itself.

func AsURN

func AsURN[N, T any](urn string) (URN[N, T], error)

URN parser, converts a string into a URN type. Input string is expected to be a compact URN of the form urn:{namespace}:{schema}. It returns an error if the schema prefix is different than one associated with type T.

func ToURN

func ToURN[N, T any](seq ...string) URN[N, T]

URN type constructor, creates a new URN from the string. It returns a valid compact URN annotated with a urn:{namespace}:{schema}.

If input segment is compact URN of other kind, it panic.

func (*URN[N, T]) FromIRI

func (urn *URN[N, T]) FromIRI(iri IRI[T])

Cast IRI to URN type.

func (URN[N, T]) IsEmpty

func (urn URN[N, T]) IsEmpty() bool

Check URN is defined.

func (*URN[N, T]) IsValid

func (urn *URN[N, T]) IsValid() bool

Validate URN to be a valid URN type.

func (URN[N, T]) Join

func (urn URN[N, T]) Join(other URN[N, T]) URN[N, T]

Join combines two URNs into one, where the second URN is appended to the first.

func (URN[N, T]) MarshalJSON

func (urn URN[N, T]) MarshalJSON() ([]byte, error)

Encodes IRI into JSON as a CURIE string.

func (URN[N, T]) Norm

func (urn URN[N, T]) Norm() URN[N, T]

Normalize URN to a URN type. Use it as constructor for URN types aliases.

type MyID = gold.URN[MyNamespace, MyClass]
var id = MyID("foo").Norm()

func (URN[N, T]) Reference

func (urn URN[N, T]) Reference() string

Reference returns a local identifier of the URN, without schema prefix.

func (URN[N, T]) Split

func (urn URN[N, T]) Split() (URN[N, T], URN[N, T])

Split splits URN into two parts: base and reference.

urn:{namespace}:{schema}:{id⁰}:{id¹}:...:{idⁿ} ⇒
  urn:{namespace}:{schema}:{id⁰}:{id¹}:...:{idⁿ⁻¹}
  urn:{namespace}:{schema}:{idⁿ}

func (URN[N, T]) ToIRI

func (urn URN[N, T]) ToIRI() IRI[T]

Cast URN to IRI type.

func (*URN[N, T]) UnmarshalJSON

func (urn *URN[N, T]) UnmarshalJSON(b []byte) (err error)

Decodes IRI from JSON as a CURIE string.

func (*URN[N, T]) Validate

func (urn *URN[N, T]) Validate() (err error)

Validate URN to be a valid compact URN type, return an error if it is not.

type Url1

type Url1[T ~string, A any] string

Url1 is a type-safe URL template with one parameter. T is the target string type, A is the type of the parameter.

Example:

type UserUrl = gold.Url1[string, UserID]
const UserProfile = UserUrl("/users/%s")
url := UserProfile.Build(userID)

func (Url1[T, A]) Build

func (url Url1[T, A]) Build(a A) T

Build constructs the URL by formatting the template with the provided parameter.

type Url2

type Url2[T ~string, A, B any] string

Url2 is a type-safe URL template with two parameters. T is the target string type, A and B are the types of the parameters.

Example:

type StoryUrl = gold.Url2[string, AccountID, StoryID]
const CatalogStoryUrl = StoryUrl("/catalog/authors/%s/stories/%s")
url := CatalogStoryUrl.Build(accountID, storyID)

func (Url2[T, A, B]) Build

func (url Url2[T, A, B]) Build(a A, b B) T

Build constructs the URL by formatting the template with the provided parameters.

type Url3

type Url3[T ~string, A, B, C any] string

Url3 is a type-safe URL template with three parameters. T is the target string type, A, B, and C are the types of the parameters.

Example:

type CommentUrl = gold.Url3[string, AccountID, StoryID, CommentID]
const StoryCommentUrl = CommentUrl("/catalog/authors/%s/stories/%s/comments/%s")
url := StoryCommentUrl.Build(accountID, storyID, commentID)

func (Url3[T, A, B, C]) Build

func (url Url3[T, A, B, C]) Build(a A, b B, c C) T

Build constructs the URL by formatting the template with the provided parameters.

type Url4

type Url4[T ~string, A, B, C, D any] string

Url4 is a type-safe URL template with four parameters. T is the target string type, A, B, C, and D are the types of the parameters.

Example:

type ThreadUrl = gold.Url4[string, AccountID, StoryID, CommentID, ThreadID]
const CommentThreadUrl = ThreadUrl("/catalog/authors/%s/stories/%s/comments/%s/threads/%s")
url := CommentThreadUrl.Build(accountID, storyID, commentID, threadID)

func (Url4[T, A, B, C, D]) Build

func (url Url4[T, A, B, C, D]) Build(a A, b B, c C, d D) T

Build constructs the URL by formatting the template with the provided parameters.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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