Skip to content

Repository files navigation

cask

Cache, abstracted simply.

cask is a small, driver-based caching abstraction for Go. It defines a tiny set of interfaces for cache operations and lets concrete backends (Redis, in-memory, etc.) plug in as drivers. Your application talks to a single Namespace value while the actual storage backend is resolved automatically from the client you hand in.

Features

  • Driver-based – backends register themselves and are matched to a client by type, so swapping backends needs no code changes at the call site.
  • Namespaces – build hierarchical, colon-joined keys (root:child:leaf) for multi-tenant or multi-feature key isolation.
  • Minimal surface – the core API is just Get / Set / Del / Exists, with optional Expire / TTL for backends that support expiration.
  • Pluggable TTL semantics – durations use timex, which can express finite, zero, and positive/negative-infinite durations.

Installation

go get go.gh.ink/cask

To actually store anything you also need a driver, e.g. the Redis driver:

go get go.gh.ink/cask/redis

Quick start

package main
import (
"context""fmt""github.com/redis/go-redis/v9""go.gh.ink/cask"
_ "go.gh.ink/cask/redis"// registers the "redis" driver via init()"go.gh.ink/timex"
)
funcmain() {
// Pass any supported client; cask picks the matching driver automatically.root, err:=cask.New(redis.NewClient(&redis.Options{Addr: "localhost:6379"}), "app")
iferr!=nil {
panic(err) // errors.ErrDriverNotRegistered if no driver matches the client
}
// Derive a child namespace -> key "app:session"ns:=root.Namespace("session")
ctx:=context.Background()
_=ns.Set(ctx, []byte("hello"), timex.NewPosInfDuration())
val, _:=ns.Get(ctx)
fmt.Println(string(val)) // hello
}

Core concepts

Namespace

A Namespace is the value your code interacts with. It bundles three things:

  • the raw client you passed to cask.New (retrievable via Raw()),
  • a key path — a []string joined with : and exposed via Key(),
  • the resolved Adapter (embedded), which provides the actual cache methods.

New(client, key...) walks every registered driver and asks it to build an adapter for the given client. The first driver that accepts the client wins; if none match, errors.ErrDriverNotRegistered is returned.

Namespace(key...) returns a child namespace. It is copy-on-write: the parent is never mutated, the key path is extended, and the adapter is re-resolved against the new key. This makes it safe to fan out namespaces from a shared root.

root, _:=cask.New(client, "app") // key: "app"users:=root.Namespace("users") // key: "app:users"alice:=users.Namespace("alice") // key: "app:users:alice"fmt.Println(alice.Key()) // app:users:alice

Package layout

PackageResponsibility
cask (root)New constructor, Namespace type, and namespace chaining (Namespace, Key, Raw).
modelThe contract interfaces: BaseStore, Expirer, Adapter, Driver, NamespaceInfo.
driverThe driver registry: Register(name, driver) and List().
errorsShared sentinel errors, currently ErrDriverNotRegistered.
internal/stateProcess-wide Drivers map backing the registry (internal, not importable).
exampleA reference driver showing the minimal driver/adapter implementation.

Interfaces

Defined in the model package:

typeBaseStoreinterface {
Get(ctx context.Context) ([]byte, error)
Set(ctx context.Context, value []byte, ttl timex.Duration) errorDel(ctx context.Context) (bool, error)
Exists(ctx context.Context) (bool, error)
}
typeExpirerinterface {
Expire(ctx context.Context, ttl timex.Duration) errorTTL(ctx context.Context) (timex.Duration, error)
}
typeAdapterinterface {
BaseStore
}
// NamespaceInfo is what a driver sees: the namespaced key string.typeNamespaceInfointerface {
Key() string
}
// Driver builds an Adapter for a given client, or reports that it can't.typeDriverinterface {
NewAdapter(clientany, nsNamespaceInfo) (adapterAdapter, okbool)
}

Adapter requires BaseStore. Expirer is optional capability: backends that support TTL management implement it, and callers reach it via a type assertion, e.g. ns.Adapter.(model.Expirer).

Writing a driver

A driver is two pieces — an Adapter that performs operations and a Driver that matches a client type and constructs the adapter — plus a one-line registration. See the example package for the canonical skeleton:

// 1. Adapter implements model.Adapter (BaseStore).typeAdapterstruct{}
func (aAdapter) Get(ctx context.Context) ([]byte, error) { /* ... */ }
func (aAdapter) Set(ctx context.Context, v []byte, ttl timex.Duration) error { /* ... */ }
func (aAdapter) Del(ctx context.Context) (bool, error) { /* ... */ }
func (aAdapter) Exists(ctx context.Context) (bool, error) { /* ... */ }
// 2. Driver matches a client type and returns ok=true when it can handle it.typeDriverstruct{}
func (dDriver) NewAdapter(clientany, ns model.NamespaceInfo) (model.Adapter, bool) {
ifc, ok:=client.(MyClientType); ok {
returnAdapter{ /* keep c and ns */ }, true
}
returnnil, false
}
// 3. Register it. Drivers commonly do this in init() so a blank import is enough:funcinit() { driver.Register("myname", Driver{}) }

The bundled example driver registers via an exported Trigger() function instead of init(), so it is only active when explicitly invoked — useful as a reference without polluting the global registry. Real drivers (like cask-redis) register in init().

Errors

ErrorMeaning
errors.ErrDriverNotRegisteredNew found no registered driver willing to handle the client.

License

Apache License 2.0. See LICENSE.

About

Cache, abstracted simply.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages