Documentation
¶
Overview ¶
Package papertrail is a pure-Go (CGO=0), MRI-faithful reimplementation of the deterministic core of Ruby's paper_trail gem — the model-versioning / audit engine.
PaperTrail records a Version every time a tracked model is created, updated or destroyed. Each version carries the item's type and id, the event name, who did it ("whodunnit"), a serialized snapshot of the attributes *before* the change ("object"), and the per-attribute changeset ("object_changes", {attr => [old, new]}). Past model states can be reconstructed with [Reify], navigated with previous/next version, and queried at a point in time with Tracker.VersionAt.
Faithfulness and seams ¶
Everything PaperTrail does *around* persistence is deterministic and needs no Ruby interpreter, so it lives here as pure Go: event detection, building the object snapshot, computing the changeset from a before/after attribute map, applying the only/ignore/skip/on filters, reifying a past state, and resolving the whodunnit. The host provides the model's attribute snapshot as a plain map[string]any (before and after); the version STORE is a persistence seam (Store); the CLOCK (Clock) and the request context (RequestContext, PaperTrail.request.whodunnit) are injected. Serialization is a seam too (Serializer); the default JSONSerializer matches modern PaperTrail JSON columns (see serializer.go for why JSON over YAML). An in-memory store, an injected clock and a fixed whodunnit back the tests; an rbgo binding wires the same seams to ActiveRecord and Ruby.
Index ¶
- Constants
- Variables
- type Change
- type Clock
- type ClockFunc
- type Config
- type JSONSerializer
- type MemoryStore
- type Options
- type ReifyOption
- type RequestContext
- type Serializer
- type Store
- type Tracker
- func (t *Tracker) Live(itemID string) (bool, error)
- func (t *Tracker) NextVersion(v Version) (*Version, error)
- func (t *Tracker) PreviousVersion(v Version) (*Version, error)
- func (t *Tracker) Record(itemID string, before, after map[string]any) (*Version, error)
- func (t *Tracker) RecordCreate(itemID string, after map[string]any) (*Version, error)
- func (t *Tracker) RecordDestroy(itemID string, before map[string]any) (*Version, error)
- func (t *Tracker) RecordUpdate(itemID string, before, after map[string]any) (*Version, error)
- func (t *Tracker) Reify(v Version, opts ...ReifyOption) (map[string]any, error)
- func (t *Tracker) VersionAt(itemID string, ts time.Time) (map[string]any, bool, error)
- func (t *Tracker) Versions(itemID string) ([]Version, error)
- type Version
Constants ¶
const ( // UnversionedNil sets attributes present on the current record but absent // from the version's snapshot to nil (PaperTrail's default). UnversionedNil = "nil" // UnversionedPreserve keeps such attributes at their current value. UnversionedPreserve = "preserve" )
Unversioned-attribute handling for [Reify], mirroring PaperTrail's reify(unversioned_attributes:) option.
const ( EventCreate = "create" EventUpdate = "update" EventDestroy = "destroy" )
Event names recorded on a Version, mirroring PaperTrail's `event` column.
Variables ¶
var ErrNoAttributes = errors.New("papertrail: cannot infer event from nil before and after attributes")
ErrNoAttributes is returned by Tracker.Record when both the before and after attribute maps are nil, so no event can be inferred.
Functions ¶
This section is empty.
Types ¶
type Clock ¶
Clock is the time seam stamped onto Version.CreatedAt. Production uses SystemClock; tests inject a fixed clock for deterministic timestamps.
type Config ¶
type Config struct {
// Only, when non-empty, restricts version-triggering to changes on these
// attributes (has_paper_trail only:). Changes outside the list do not, on
// their own, create an update version.
Only []string
// Ignore lists attributes whose changes do not, on their own, trigger an
// update version (has_paper_trail ignore:). Ignored attributes are still
// serialized if another attribute triggers the version.
Ignore []string
// Skip lists attributes excluded entirely from the `object` snapshot and the
// `object_changes` changeset, and which never trigger a version
// (has_paper_trail skip:).
Skip []string
// On restricts which lifecycle events are versioned (has_paper_trail
// on: [...]). Empty means all of create/update/destroy.
On []string
}
Config mirrors the has_paper_trail options that govern *when* a version is recorded. It affects version triggering only; the skip list additionally excludes attributes from serialization.
type JSONSerializer ¶
type JSONSerializer struct{}
JSONSerializer is the default Serializer; it stores snapshots and changesets as JSON, matching PaperTrail's JSON/JSONB columns.
func (JSONSerializer) DumpChanges ¶
func (JSONSerializer) DumpChanges(m map[string]Change) (string, error)
DumpChanges implements Serializer. The changeset is emitted as {"attr": [old, new]}, PaperTrail's on-disk shape.
func (JSONSerializer) DumpObject ¶
func (JSONSerializer) DumpObject(m map[string]any) (string, error)
DumpObject implements Serializer.
func (JSONSerializer) LoadChanges ¶
func (JSONSerializer) LoadChanges(s string) (map[string]Change, error)
LoadChanges implements Serializer.
func (JSONSerializer) LoadObject ¶
func (JSONSerializer) LoadObject(s string) (map[string]any, error)
LoadObject implements Serializer.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is an in-memory Store for tests and non-persistent hosts. It is safe for the sequential access the tests perform.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore returns an empty MemoryStore.
func (*MemoryStore) SaveVersion ¶
func (s *MemoryStore) SaveVersion(v Version) (Version, error)
SaveVersion implements Store, assigning a monotonically increasing ID.
func (*MemoryStore) VersionsFor ¶
func (s *MemoryStore) VersionsFor(itemType, itemID string) ([]Version, error)
VersionsFor implements Store.
type Options ¶
type Options struct {
ItemType string
Config Config
Store Store
Clock Clock
Serializer Serializer
Request *RequestContext
}
Options configures a Tracker. Only ItemType is required; the remaining seams default to an in-memory store, the system clock, JSON serialization and a fresh enabled request context.
type ReifyOption ¶
type ReifyOption func(*reifyConfig)
ReifyOption customizes Tracker.Reify.
func ReifyUnversioned ¶
func ReifyUnversioned(mode string) ReifyOption
ReifyUnversioned selects how attributes present on the current record but absent from the snapshot are handled: UnversionedNil (default) or UnversionedPreserve.
func ReifyWithCurrent ¶
func ReifyWithCurrent(current map[string]any) ReifyOption
ReifyWithCurrent supplies the record's current attributes so that Tracker.Reify can decide how to handle attributes that were not versioned (see ReifyUnversioned).
type RequestContext ¶
type RequestContext struct {
// Whodunnit is PaperTrail.request.whodunnit — the actor stamped onto each
// recorded version.
Whodunnit string
// Enabled mirrors PaperTrail.request.enabled?; when false, no version is
// recorded. NewRequest defaults it to true.
Enabled bool
}
RequestContext is the per-request seam behind PaperTrail.request. It carries the whodunnit applied to versions recorded during the request and the enabled/disabled toggle (PaperTrail.request.enabled = false suppresses versioning). In the gem this is thread/fiber-local; here the host owns a RequestContext per logical request and passes it to New.
func NewRequest ¶
func NewRequest() *RequestContext
NewRequest returns an enabled RequestContext with an empty whodunnit.
type Serializer ¶
type Serializer interface {
// DumpObject serializes an attribute snapshot. A nil map serializes to the
// empty string (an absent snapshot, as for a create event).
DumpObject(map[string]any) (string, error)
// LoadObject is the inverse of DumpObject; the empty string loads to nil.
LoadObject(string) (map[string]any, error)
// DumpChanges serializes a {attr => [old, new]} changeset. A nil map
// serializes to the empty string.
DumpChanges(map[string]Change) (string, error)
// LoadChanges is the inverse of DumpChanges; the empty string loads to nil.
LoadChanges(string) (map[string]Change, error)
}
Serializer is the seam PaperTrail uses to (de)serialize the `object` snapshot and the `object_changes` changeset for storage. PaperTrail::Serializers::YAML is the gem's historical default (Rails text columns), but modern PaperTrail on Postgres/MySQL uses native JSON/JSONB columns via PaperTrail::Serializers::JSON. We adopt JSON as the default (JSONSerializer): it is stdlib-only (encoding/json, CGO=0), round-trips deterministically (map keys are emitted sorted), and matches the JSON-column deployment. The seam lets an rbgo binding plug a YAML serializer when a legacy YAML column must be matched byte-for-byte.
type Store ¶
type Store interface {
// SaveVersion persists v, assigns and returns it with its ID populated.
SaveVersion(v Version) (Version, error)
// VersionsFor returns every stored version for the given item, ordered
// oldest-first (ascending ID).
VersionsFor(itemType, itemID string) ([]Version, error)
}
Store is the persistence seam for versions. In production an rbgo binding backs it with ActiveRecord (the `versions` table); tests use MemoryStore.
type Tracker ¶
type Tracker struct {
ItemType string
Config Config
Store Store
Clock Clock
Serializer Serializer
Request *RequestContext
}
Tracker is the has_paper_trail engine for one model class. It builds versions from before/after attribute maps, persists them through the Store seam, and answers version queries. It is the Go surface an rbgo binding drives from the model's create/update/destroy callbacks.
func (*Tracker) Live ¶
Live reports whether the item currently exists as a live record, i.e. its latest version is not a destroy. An item with no versions is treated as live.
func (*Tracker) NextVersion ¶
NextVersion returns the version immediately following v for the same item, or nil if v is the latest (the item is at its live state).
func (*Tracker) PreviousVersion ¶
PreviousVersion returns the version immediately preceding v for the same item, or nil if v is the earliest.
func (*Tracker) Record ¶
Record infers the event from the presence of before/after attributes (before-only ⇒ destroy, after-only ⇒ create, both ⇒ update) and records it.
func (*Tracker) RecordCreate ¶
RecordCreate records a create version for a newly created record.
func (*Tracker) RecordDestroy ¶
RecordDestroy records a destroy version for a record being deleted.
func (*Tracker) RecordUpdate ¶
RecordUpdate records an update version, or returns (nil, nil) when the change is a no-op under the only/ignore/skip filters.
func (*Tracker) Reify ¶
Reify reconstructs the attribute state captured by v's `object` snapshot — the record as it was *before* v's change. A create version has no prior state and reifies to nil. When ReifyWithCurrent is supplied, attributes on the current record that were not versioned are set to nil, or preserved under ReifyUnversioned(UnversionedPreserve).
type Version ¶
type Version struct {
// ID is the store-assigned primary key. It is zero until a [Store] persists
// the version; ordering by ID reproduces PaperTrail's insertion order.
ID int64
// ItemType / ItemID identify the tracked record (STI base class name and id
// in the gem).
ItemType string
ItemID string
// Event is one of [EventCreate], [EventUpdate], [EventDestroy].
Event string
// Whodunnit is the actor responsible for the change, resolved from the
// [RequestContext] at record time.
Whodunnit string
// Object is the serialized snapshot of the attributes *before* the change:
// empty for create (there was no prior state), the pre-update attributes for
// update, and the pre-destroy attributes for destroy.
Object string
// ObjectChanges is the serialized {attr => [old, new]} changeset. It is set
// for create and update, and empty for destroy (matching the gem, which
// records no changeset when a record is wiped).
ObjectChanges string
// CreatedAt is the [Clock] time the version was recorded.
CreatedAt time.Time
}
Version is the audit record PaperTrail writes for each tracked change. It maps one-to-one onto the gem's `versions` table columns.
