contract

package module
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Jan 8, 2026 License: MIT Imports: 4 Imported by: 8

README

Cosmos: Contract

Service interfaces and helper utilities for building HTTP applications in Go. This module provides a collection of common interfaces (Cache, Database, Session, Crypto, Hash) with zero dependencies, designed for dependency injection and testing.

Overview

The contract module serves as the foundation for Cosmos, providing:

  • Service Interfaces: Common abstractions for caching, databases, sessions, encryption, and hashing
  • Request Helpers: Functions for working with HTTP requests (headers, cookies, params, query strings, body parsing)
  • Response Helpers: Functions for writing HTTP responses (JSON, streams, static files)
  • Hooks System: Lifecycle hooks for middleware to inject behavior
  • Mock Implementations: Generated mocks for all interfaces via mockery

This module has zero dependencies (except testing libraries) and can be used standalone or with any framework.

Installation

go get github.com/studiolambda/cosmos/contract

Service Interfaces

Cache

Generic cache interface inspired by Laravel's Cache Repository. Supports CRUD operations, atomic operations, and lazy-loading via Remember patterns.

type Cache interface {
    Get(ctx context.Context, key string) (any, error)
    Put(ctx context.Context, key string, value any, ttl time.Duration) error
    Delete(ctx context.Context, key string) error
    Has(ctx context.Context, key string) (bool, error)
    Pull(ctx context.Context, key string) (any, error)
    Forever(ctx context.Context, key string, value any) error
    Increment(ctx context.Context, key string, by int64) (int64, error)
    Decrement(ctx context.Context, key string, by int64) (int64, error)
    Remember(ctx context.Context, key string, ttl time.Duration, compute func() (any, error)) (any, error)
    RememberForever(ctx context.Context, key string, compute func() (any, error)) (any, error)
}

Standard Errors:

  • ErrCacheKeyNotFound: Key does not exist in cache
  • ErrCacheUnsupportedOperation: Operation not supported by backend
Database

Generic SQL database interface with support for queries, transactions, and named parameters.

type Database interface {
    Close() error
    Ping(ctx context.Context) error
    Exec(ctx context.Context, query string, args ...any) (int64, error)
    ExecNamed(ctx context.Context, query string, arg any) (int64, error)
    Select(ctx context.Context, query string, dest any, args ...any) error
    SelectNamed(ctx context.Context, query string, dest any, arg any) error
    Find(ctx context.Context, query string, dest any, args ...any) error
    FindNamed(ctx context.Context, query string, dest any, arg any) error
    WithTransaction(ctx context.Context, fn func(tx Database) error) error
}

Standard Errors:

  • ErrDatabaseNoRows: No rows found
  • ErrDatabaseNestedTransaction: Nested transaction attempted
Session

User session interface with data storage and lifecycle management.

type Session interface {
    SessionID() string
    OriginalSessionID() string
    Get(key string) (any, bool)
    Put(key string, value any)
    Delete(key string)
    Extend(expiresAt time.Time)
    Regenerate() error
    Clear()
    ExpiresAt() time.Time
    HasExpired() bool
    ExpiresSoon(delta time.Duration) bool
    HasChanged() bool
    HasRegenerated() bool
    MarkAsUnchanged()
}

type SessionDriver interface {
    Get(ctx context.Context, id string) (Session, error)
    Save(ctx context.Context, session Session, ttl time.Duration) error
    Delete(ctx context.Context, id string) error
}
Crypto

Encryption interface for authenticated encryption.

type Crypto interface {
    Encrypt(ctx context.Context, plaintext []byte) ([]byte, error)
    Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error)
}
Hash

Password hashing interface.

type Hash interface {
    Hash(ctx context.Context, password []byte) ([]byte, error)
    Verify(ctx context.Context, password, hash []byte) error
}

Request Helpers

Headers
// Get request header value
value := request.Header(r, "Authorization")

// Get all header values
values := request.Headers(r, "Accept-Language")
Cookies
// Get cookie value
value, err := request.Cookie(r, "session_id")

// Check if cookie exists
exists := request.HasCookie(r, "preferences")
Path Parameters
// Get path parameter from route pattern
// Example: /users/{id}
id := request.Param(r, "id")
Query Strings
// Get single query parameter
page := request.Query(r, "page")

// Get all values for a query parameter
tags := request.Queries(r, "tags")

// Check if query parameter exists
hasFilter := request.HasQuery(r, "filter")
Body Parsing
// Parse JSON body
var user User
if err := request.Body(r, &user); err != nil {
    return err
}
Hooks
// Access request hooks
hooks := request.Hooks(r)

// Add hook before status is written
hooks.BeforeWriteHeader(func(w http.ResponseWriter, status int) {
    log.Printf("Status: %d", status)
})

// Add hook after response completes
hooks.AfterResponse(func(err error) {
    log.Printf("Request completed with error: %v", err)
})
Sessions
// Get session from request (requires session middleware)
session := request.Session(r)

// Check if request has session
if request.HasSession(r) {
    session := request.Session(r)
    userID, ok := session.Get("user_id")
}

Response Helpers

JSON Responses
// Send JSON response
type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

user := User{ID: 1, Name: "Alice"}
return response.JSON(w, http.StatusOK, user)
Stream Responses
// Stream data with custom content type
reader := strings.NewReader("streaming data")
return response.Stream(w, http.StatusOK, "text/plain", reader)
Static Files
// Serve static file
return response.Static(w, r, "/path/to/file.pdf")

Hooks System

The hooks system provides lifecycle events for middleware:

type Hooks interface {
    BeforeWriteHeader(fn func(w http.ResponseWriter, status int))
    BeforeWrite(fn func(w http.ResponseWriter, data []byte))
    AfterResponse(fn func(err error))
}

Hook Execution:

  1. BeforeWriteHeader: Called before HTTP status is written (can be used to inspect/modify status)
  2. BeforeWrite: Called before response body is written (can be used to inspect/modify data)
  3. AfterResponse: Called after response completes, even if an error occurred (useful for logging)

Testing with Mocks

The contract module includes generated mocks for all interfaces:

import "github.com/studiolambda/cosmos/contract/mock"

// Create mock cache
mockCache := &mock.Cache{}
mockCache.On("Get", mock.Anything, "key").Return("value", nil)

// Use in tests
service := NewService(mockCache)
result, err := service.FetchData()

// Verify expectations
mockCache.AssertExpectations(t)
Generating Mocks

Mocks are generated using mockery:

cd contract/
go generate ./...

Configuration is in .mockery.yml.

Usage Examples

Using Cache Interface
func fetchUserData(ctx context.Context, cache contract.Cache, userID string) (*User, error) {
    // Try to get from cache
    key := fmt.Sprintf("user:%s", userID)
    
    // Use Remember pattern for lazy-loading
    data, err := cache.Remember(ctx, key, 1*time.Hour, func() (any, error) {
        // Fetch from database if not cached
        return fetchUserFromDB(userID)
    })
    
    if err != nil {
        return nil, err
    }
    
    return data.(*User), nil
}
Using Database Interface
func createUser(ctx context.Context, db contract.Database, user *User) error {
    query := `INSERT INTO users (name, email) VALUES ($1, $2)`
    
    _, err := db.Exec(ctx, query, user.Name, user.Email)
    return err
}

func getUserByEmail(ctx context.Context, db contract.Database, email string) (*User, error) {
    query := `SELECT id, name, email FROM users WHERE email = $1`
    
    var user User
    if err := db.Find(ctx, query, &user, email); err != nil {
        return nil, err
    }
    
    return &user, nil
}
Using Session Interface
func login(w http.ResponseWriter, r *http.Request) error {
    session := request.Session(r)
    
    // Authenticate user
    user, err := authenticateUser(r)
    if err != nil {
        return err
    }
    
    // Store user ID in session
    session.Put("user_id", user.ID)
    
    // Regenerate session to prevent fixation
    if err := session.Regenerate(); err != nil {
        return err
    }
    
    return response.JSON(w, http.StatusOK, user)
}

Design Philosophy

The contract module follows these principles:

  1. Zero Dependencies: Can be used with any framework or standalone
  2. Interface-Based: All services are interfaces for maximum flexibility
  3. Standard Patterns: Inspired by proven patterns from Laravel and other frameworks
  4. Testing First: Includes mocks for all interfaces out of the box
  5. Context-Aware: All operations accept context.Context for cancellation and timeouts

Error Handling

All errors returned from contract interfaces should be checked. Standard errors are defined as variables that can be compared using errors.Is():

value, err := cache.Get(ctx, "key")
if errors.Is(err, contract.ErrCacheKeyNotFound) {
    // Handle missing key
}

License

MIT License - Copyright (c) 2025 Erik C. Fores

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrCacheKeyNotFound should be returned when a key does not exist in the cache.
	// Additional context should also be supplied, such as the cache key.
	ErrCacheKeyNotFound = errors.New("cache key not found")

	// ErrCacheUnsupportedOperation should be returned when a method (e.g., Forever or atomic ops) is not supported by the backend.
	// Additional context should also be supplied, such as the operation name.
	ErrCacheUnsupportedOperation = errors.New("cache unsupported operation")
)
View Source
var (
	// ErrDatabaseNoRows is the error that should be returned
	// when there's no rows found.
	ErrDatabaseNoRows = errors.New("no database rows were found")

	// ErrDatabaseNestedTransaction is the error that should be returned
	// when attempting to create a nested transaction, which is not supported.
	ErrDatabaseNestedTransaction = errors.New("nested transactions are not supported")
)
View Source
var HooksKey = hooksKey{}
View Source
var SessionKey = sessionKey{}

SessionKey is the context key used to store and retrieve the session from a context.Context.

Functions

This section is empty.

Types

type AfterResponseHook added in v0.8.0

type AfterResponseHook func(err error)

type BeforeWriteHeaderHook added in v0.8.0

type BeforeWriteHeaderHook func(w http.ResponseWriter, status int)

type BeforeWriteHook added in v0.8.0

type BeforeWriteHook func(w http.ResponseWriter, content []byte)

type Cache

type Cache interface {
	// Get retrieves the value for the given key.
	// Returns nil and an error if the key is missing or retrieval fails.
	Get(ctx context.Context, key string) (any, error)

	// Put stores a value for the given key with a TTL.
	// Overwrites any existing value.
	Put(ctx context.Context, key string, value any, ttl time.Duration) error

	// Delete removes the cached value for the given key.
	// Does nothing if the key does not exist.
	Delete(ctx context.Context, key string) error

	// Has returns true if the key exists in the cache and is not expired.
	Has(ctx context.Context, key string) (bool, error)

	// Pull retrieves and removes the value for the given key.
	// Returns nil and an error if the key is missing.
	Pull(ctx context.Context, key string) (any, error)

	// Forever stores a value permanently (no TTL).
	// Only supported if the backend allows non-expiring keys.
	Forever(ctx context.Context, key string, value any) error

	// Increment atomically increases the integer value stored at key by the given
	// amount. Returns the new value or an error if the operation fails.
	Increment(ctx context.Context, key string, by int64) (int64, error)

	// Decrement atomically decreases the integer value stored at key by
	// the given amount. Returns the new value or an error if the operation fails.
	Decrement(ctx context.Context, key string, by int64) (int64, error)

	// Remember attempts to get the value for the given key.
	// If the key is missing, it calls the compute function, stores the result with the given TTL, and returns it.
	Remember(ctx context.Context, key string, ttl time.Duration, compute func() (any, error)) (any, error)

	// RememberForever is like Remember but stores the computed result without TTL (permanently).
	RememberForever(ctx context.Context, key string, compute func() (any, error)) (any, error)
}

Cache defines a generic cache contract inspired by Laravel's Cache Repository. It supports basic CRUD, atomic operations, and lazy-loading via Remember patterns.

type Database

type Database interface {
	// Close closes the database connection and releases any associated resources.
	// It should be called when the database is no longer needed to prevent
	// resource leaks. After calling Close, the Database instance should not
	// be used for further operations.
	Close() error

	// Ping verifies that the database connection is still alive and accessible.
	// It sends a simple query to the database to test connectivity and returns
	// an error if the connection is unavailable or the database is unreachable.
	// This method is typically used for health checks and connection validation.
	// Ping will also connect to the database if the connection was not established.
	Ping(ctx context.Context) error

	// Exec executes a SQL query that modifies data (e.g., INSERT, UPDATE, DELETE).
	// It returns the number of rows affected.
	Exec(ctx context.Context, query string, args ...any) (int64, error)

	// ExecNamed executes a SQL query that modifies data using named parameters.
	// The arg parameter should be a struct or map containing the named parameters.
	// Returns the number of rows affected.
	ExecNamed(ctx context.Context, query string, arg any) (int64, error)

	// Select executes a query and scans the results into dest.
	// Dest should be a pointer to a slice of structs (e.g., *[]User).
	// Returns an error if the query fails or if scanning is unsuccessful.
	Select(ctx context.Context, query string, dest any, args ...any) error

	// SelectNamed executes a query using named parameters and scans the results into dest.
	// Dest should be a pointer to a slice of structs, and arg should contain the named parameters.
	// Returns an error if the query fails or if scanning is unsuccessful.
	SelectNamed(ctx context.Context, query string, dest any, arg any) error

	// Find executes a query expected to return a single row,
	// and scans the result into dest. Dest should be a pointer to a struct.
	// Returns an error if no row is found, multiple rows are returned, or scanning fails.
	Find(ctx context.Context, query string, dest any, args ...any) error

	// FindNamed executes a query using named parameters expected to return a single row,
	// and scans the result into dest. Dest should be a pointer to a struct,
	// and arg should contain the named parameters.
	// Returns an error if no row is found, multiple rows are returned, or scanning fails.
	FindNamed(ctx context.Context, query string, dest any, arg any) error

	// WithTransaction executes the provided function fn within a database transaction.
	// If fn returns an error, the transaction is rolled back. Otherwise, it is committed.
	// The tx passed to fn implements the same Database interface and can be used
	// for nested operations within the transaction.
	WithTransaction(ctx context.Context, fn func(tx Database) error) error
}

Database defines a generic interface for interacting with a SQL-based datastore. It provides methods for executing queries, retrieving multiple or single records, and performing operations within a transaction context.

type Encrypter added in v0.5.0

type Encrypter interface {
	// Encrypt takes a byte slice and returns an encrypted version of it.
	// It returns an error if the encryption operation fails.
	Encrypt(value []byte) ([]byte, error)

	// Decrypt takes an encrypted byte slice and returns the decrypted original value.
	// It returns an error if the decryption operation fails.
	Decrypt(value []byte) ([]byte, error)
}

Encrypter defines the interface for encrypting and decrypting data. Implementations of Encrypter are responsible for securing data through encryption and recovering the original data through decryption.

type EventHandler added in v0.8.0

type EventHandler = func(payload EventPayload)

type EventPayload added in v0.8.0

type EventPayload = func(dest any) error

type EventUnsubscribeFunc added in v0.8.0

type EventUnsubscribeFunc = func() error

type Events added in v0.8.0

type Events interface {
	Publish(ctx context.Context, event string, payload any) error
	Subscribe(ctx context.Context, event string, handler EventHandler) (EventUnsubscribeFunc, error)
	Close() error
}

type Hasher added in v0.5.0

type Hasher interface {
	// Hash computes a cryptographic hash of the given byte slice and returns the hash.
	// It returns an error if the hashing operation fails.
	Hash(value []byte) ([]byte, error)

	// Check verifies that the given value matches the provided hash.
	// It returns true if the value and hash match, false otherwise.
	// It returns an error if the verification operation fails.
	Check(value []byte, hash []byte) (bool, error)
}

Hasher defines the interface for hashing and verifying hashed values. Implementations of Hasher are responsible for generating cryptographic hashes and verifying that values match their corresponding hashes.

type Hooks added in v0.8.0

type Hooks interface {
	AfterResponse(callbacks ...AfterResponseHook)
	AfterResponseFuncs() []AfterResponseHook
	BeforeWrite(callbacks ...BeforeWriteHook)
	BeforeWriteFuncs() []BeforeWriteHook
	BeforeWriteHeader(callbacks ...BeforeWriteHeaderHook)
	BeforeWriteHeaderFuncs() []BeforeWriteHeaderHook
}

type Session added in v0.5.0

type Session interface {
	// SessionID returns the current session identifier. This may differ from the original
	// session ID if the session has been regenerated.
	SessionID() string

	// OriginalSessionID returns the session ID that was originally assigned to this session.
	// This remains constant even if the session is regenerated.
	OriginalSessionID() string

	// Get retrieves a value from the session by key. It returns the value and a boolean
	// indicating whether the key exists in the session. The value can be of any type.
	Get(key string) (any, bool)

	// Put stores a value in the session associated with the given key. If the key already
	// exists, its value is overwritten.
	Put(key string, value any)

	// Delete removes the value associated with the given key from the session.
	// If the key does not exist, this operation is a no-op.
	Delete(key string)

	// Extend updates the session's expiration time to the specified time. This is useful
	// for extending a session's lifetime during active use.
	Extend(expiresAt time.Time)

	// Regenerate creates a new session ID and associates it with this session.
	// This is commonly used after authentication to prevent session
	// fixation attacks. It returns an error if the regeneration process fails.
	Regenerate() error

	// Clear removes all data from the session while maintaining the session itself.
	Clear()

	// ExpiresAt returns the time at which the session will expire.
	ExpiresAt() time.Time

	// HasExpired returns true if the current time is past the session's expiration time.
	HasExpired() bool

	// ExpiresSoon returns true if the session will expire within the specified duration
	// from the current time. This is useful for triggering session renewal prompts.
	ExpiresSoon(delta time.Duration) bool

	// HasChanged returns true if the session data has been modified since it was loaded.
	// This is useful for determining whether the session needs to be persisted.
	HasChanged() bool

	// HasRegenerated returns true if the session has been regenerated (i.e., the session ID
	// has changed). This is useful for sending updated session identifiers to the client.
	HasRegenerated() bool

	// MarkAsUnchanged sets the session as if nothing has changed, therefore avoiding saving
	// the session when the request finishes.
	MarkAsUnchanged()
}

Session represents a user session with data storage and lifecycle management capabilities. It provides methods to store, retrieve, and manage session data, as well as control session expiration and regeneration for security purposes.

type SessionDriver added in v0.5.0

type SessionDriver interface {
	// Get retrieves a session from persistent storage by its ID. It returns the session
	// and an error if the session cannot be found or if retrieval fails. If a session
	// with the given ID does not exist, an error should be returned.
	Get(ctx context.Context, id string) (Session, error)

	// Save persists a session to storage with the specified time-to-live (TTL).
	// The TTL parameter indicates how long the session should be retained in storage
	// before it can be automatically removed. It returns an error if the save operation fails.
	Save(ctx context.Context, session Session, ttl time.Duration) error

	// Delete removes a session from persistent storage by its ID. It returns an error
	// if the delete operation fails.
	Delete(ctx context.Context, id string) error
}

SessionDriver defines the interface for persisting and retrieving session data. Implementations of SessionDriver are responsible for managing session storage, such as storing sessions in a database, cache, or file system.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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