factorybot

package module
v0.0.0-...-e60c066 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 3 Imported by: 0

README

go-ruby-factory-bot/factory-bot

factory-bot — go-ruby-factory-bot

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the deterministic core of Ruby's factory_bot gem — the fixtures-replacement library used to define factories and construct objects for tests and seed data. It reproduces the factory registry, attribute resolution, sequences, traits, associations, transient attributes, parent/child inheritance, nested factories, and the build/create callback pipeline — without any Ruby runtime.

It is the factory_bot engine for go-embedded-ruby, but a standalone, reusable module.

What it is — and isn't. Everything factory_bot does to resolve a factory into a bag of attributes is deterministic and needs no interpreter, so it lives here as pure Go: merging parent attributes, overlaying traits, evaluating static and dynamic (lazy) values, advancing sequences, resolving associations, separating transient attributes, and driving the callback pipeline in factory_bot's order (after(:build)before(:create) → persist → after(:create)). The two things factory_bot delegates to the object model — instantiating the class and persisting it — are host seams: BuildFunc(class, attrs) builds the object (factory_bot's initialize_with + attribute assignment) and PersistFunc(class, obj) saves it (to_create, default save!). A dynamic attribute value is the block seam Block (func(*Evaluator) any), mirroring attr { ... }. The default seams return the attribute map and a no-op, so the whole engine is exercised with no ORM. A future rbgo binding wires the seams to Ruby object construction and ActiveRecord.

Features

Faithful port of factory_bot's definition + resolution core:

  • RegistryNew(); Define(name, func(*Definition)) mirrors FactoryBot.define { factory :user do … end }. Default() exposes a process-wide singleton (the FactoryBot module) and Reset() reloads it.
  • Attributes — static (Attr), dynamic/lazy (Dynamic, the Block seam, memoized per build and able to read siblings via the Evaluator).
  • Sequences — per-factory (Sequence/SequenceFrom) and global (Registry.Sequence + Generate), each a shared, monotonically increasing counter with an optional generator.
  • Traits — named attribute + callback overlays (Trait), applied at build time with WithTrait("admin"); inherited from parents.
  • AssociationsAssociation(name, factory, overrides, traits…), resolved by building/creating the referenced factory with the parent strategy (build within build, create within create), with cycle detection.
  • Transient attributesTransient, available to other attributes and callbacks through the evaluator but never passed to the BuildFunc seam.
  • Inheritance & nestingParent(name) and nested Factory(name, …); child attributes/traits/callbacks/class inherit and override, with parent-cycle detection.
  • CallbacksAfterBuild / BeforeCreate / AfterCreate, run in factory_bot's order; any callback error aborts the build.
  • StrategiesBuild / Create / AttributesFor / BuildList / CreateList, plus With / WithAttrs runtime overrides.
  • SeamsSetBuild(BuildFunc) and SetPersist(PersistFunc); the core never touches an object model or a database itself.
  • Error treeerrors.Is-matchable sentinels: ErrUnknownFactory, ErrUnknownTrait, ErrUnknownSequence, ErrSequenceOverflow, ErrDuplicateFactory, ErrParentCycle, ErrAssociationCycle.

CGO-free, dependency-free (stdlib only), 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x — big-endian) plus js/wasm and wasip1/wasm.

Install

go get github.com/go-ruby-factory-bot/factory-bot

Usage

package main

import (
	"fmt"

	factorybot "github.com/go-ruby-factory-bot/factory-bot"
)

func main() {
	r := factorybot.New()

	_ = r.Define("account", func(d *factorybot.Definition) {
		d.Class("Account")
		d.Attr("plan", "free")
		d.Trait("pro", func(t *factorybot.Definition) { t.Attr("plan", "pro") })
	})

	_ = r.Define("user", func(d *factorybot.Definition) {
		d.Class("User")
		d.Sequence("email", func(n int) any { return fmt.Sprintf("user%d@example.com", n) })
		d.Attr("first", "Ada")
		d.Dynamic("greeting", func(e *factorybot.Evaluator) any {
			return "Hi " + e.Get("first").(string)
		})
		d.Transient(func(t *factorybot.Definition) { t.Attr("admin", false) })
		d.Association("account", "account", nil, "pro")

		d.AfterBuild(func(obj any, e *factorybot.Evaluator) error {
			// obj is whatever BuildFunc returned; e reads transients.
			return nil
		})

		d.Factory("admin_user", func(f *factorybot.Definition) { // nested, inherits user
			f.Attr("first", "Grace")
		})
	})

	u, _ := r.Build("user")                                   // build(:user)
	admin, _ := r.Create("admin_user", factorybot.With("first", "G")) // create(:admin_user, first: "G")
	list, _ := r.BuildList("user", 3)                         // build_list(:user, 3)
	attrs, _ := r.AttributesFor("user")                       // attributes_for(:user)

	fmt.Println(u, admin, len(list), attrs["greeting"])
}
Injecting the object-model and persistence seams (hosts / rbgo)
r := factorybot.New()
r.SetBuild(func(class string, attrs map[string]any) (any, error) {
	return myORM.New(class, attrs) // initialize_with + attribute assignment
})
r.SetPersist(func(class string, obj any) error {
	return myORM.Save(obj) // to_create (default save!)
})

Value model

gem this package
FactoryBot.define Registry.Define(name, func(*Definition))
factory :user, class:, parent: do … end d.Class(...) / d.Parent(...) / nested d.Factory
name "x" / name { … } d.Attr(name, x) / d.Dynamic(name, Block)
`sequence(:email) { n
trait :admin do … end d.Trait(name, func(*Definition))
association :account d.Association(name, factory, overrides, traits…)
transient do … end d.Transient(func(*Definition))
after(:build) / before/after(:create) d.AfterBuild / d.BeforeCreate / d.AfterCreate
build/create/attributes_for/*_list Build/Create/AttributesFor/BuildList/CreateList
build(:user, :admin, name: "x") Build("user", WithTrait("admin"), With("name","x"))
initialize_with / attribute assignment BuildFunc seam (SetBuild)
to_create (default save!) PersistFunc seam (SetPersist)
FactoryBot::Errors subtree Err* sentinels (errors.Is)

Tests & coverage

The suite is deterministic and pure: the default BuildFunc/PersistFunc seams (map-returning / no-op) and stub seams drive every branch — factory build/create, sequences (including overflow), traits, associations (including cycles), transient attributes, inheritance, nested factories, the full callback pipeline, and every error sentinel. No test needs an ORM or a database, so the cross-arch qemu lanes and the Windows lane all hold coverage at 100%.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-factory-bot/factory-bot authors.

Documentation

Overview

Package factorybot is a pure-Go (CGO-free) reimplementation of the deterministic core of Ruby's factory_bot gem — the fixtures-replacement library used to define factories and construct objects for tests and seeding. It reproduces the factory registry, attribute resolution, sequences, traits, associations, transient attributes, parent/child inheritance, nested factories, and the build/create callback pipeline — without any Ruby runtime.

It is the factory_bot engine for [go-embedded-ruby](https://github.com/go-embedded-ruby/ruby), but a standalone, reusable module.

What it is — and isn't

Everything factory_bot does to resolve a factory into a bag of attributes is deterministic and needs no interpreter, so it lives here as pure Go: merging parent attributes, overlaying traits, evaluating static and dynamic (lazy) attribute values, advancing sequences, resolving associations, separating transient attributes, and driving the callback pipeline in factory_bot's order (after(:build) → before(:create) → persist → after(:create)).

The two things factory_bot delegates to the object model — instantiating the class and persisting it — are host seams here:

  • BuildFunc instantiates a class from a resolved, non-transient attribute map and returns the object (factory_bot's initialize_with / attribute assignment). The default seam returns the attribute map itself, so the library is fully exercised without any ORM.
  • PersistFunc saves a built object (factory_bot's to_create, default save!). The default seam is a no-op.

A dynamic attribute value is a Go block seam, Block (func(*Evaluator) any), mirroring factory_bot's `attr { ... }`. A future rbgo binding wires BuildFunc/PersistFunc/Block to Ruby object construction, ActiveRecord persistence, and Ruby blocks respectively.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnknownFactory is returned when a factory name is not registered.
	ErrUnknownFactory = errors.New("factorybot: unknown factory")

	// ErrUnknownTrait is returned when a requested trait is not defined on
	// the factory (or any of its ancestors).
	ErrUnknownTrait = errors.New("factorybot: unknown trait")

	// ErrUnknownSequence is returned by [Registry.Generate] for a global
	// sequence that was never registered.
	ErrUnknownSequence = errors.New("factorybot: unknown sequence")

	// ErrSequenceOverflow is returned when a sequence counter can no longer
	// be advanced without overflowing int64.
	ErrSequenceOverflow = errors.New("factorybot: sequence overflow")

	// ErrDuplicateFactory is returned by [Registry.Define] when a factory
	// with the same name is already registered.
	ErrDuplicateFactory = errors.New("factorybot: duplicate factory")

	// ErrParentCycle is returned when resolving a factory whose parent chain
	// forms a cycle.
	ErrParentCycle = errors.New("factorybot: parent cycle")

	// ErrAssociationCycle is returned when building a factory whose
	// association graph forms a cycle.
	ErrAssociationCycle = errors.New("factorybot: association cycle")
)

Sentinel errors returned by the registry. Match them with errors.Is.

They mirror the exceptions factory_bot raises: a missing factory or trait (FactoryBot::Errors), a sequence that can no longer advance, a duplicate definition (DuplicateDefinitionError), a parent chain that loops, and an association graph that loops (which factory_bot would otherwise blow the stack on).

Functions

func Reset

func Reset()

Reset clears the default registry, mirroring FactoryBot.reload for tests.

Types

type Block

type Block func(e *Evaluator) any

Block is the seam for a dynamic (lazy) attribute value, mirroring factory_bot's `name { ... }`. It is evaluated at build time and may read sibling attributes through the Evaluator it is handed (which memoizes each value, exactly like factory_bot's evaluator).

type BuildFunc

type BuildFunc func(class string, attrs map[string]any) (any, error)

BuildFunc is the object-instantiation seam, mirroring factory_bot's initialize_with plus attribute assignment. It receives the resolved class name and the non-transient attribute map and returns the constructed object. The default seam returns a copy of the attribute map, so the engine runs without any object model.

type Callback

type Callback func(obj any, e *Evaluator) error

Callback is a build/create lifecycle hook, mirroring factory_bot's after(:build) / before(:create) / after(:create). It receives the object produced by the BuildFunc seam and the Evaluator, so it can read transient attributes. Returning a non-nil error aborts the build.

type Definition

type Definition struct {
	// contains filtered or unexported fields
}

Definition is the builder handed to a define/factory/trait/transient block. It records a factory's class, parent, attributes, sequences, associations, traits, nested factories, and callbacks. The same type backs traits and transient blocks (which only use a subset of its methods).

func (*Definition) AfterBuild

func (d *Definition) AfterBuild(cb Callback) *Definition

AfterBuild registers an after(:build) callback.

func (*Definition) AfterCreate

func (d *Definition) AfterCreate(cb Callback) *Definition

AfterCreate registers an after(:create) callback.

func (*Definition) Association

func (d *Definition) Association(name, factoryName string, overrides map[string]any, traits ...string) *Definition

Association declares an association to another factory, mirroring `association :author, factory: :user, name: "x"`. An empty factoryName defaults to name. The association is built with the parent strategy (build within build, create within create). overrides and traits are applied to the associated build.

func (*Definition) Attr

func (d *Definition) Attr(name string, value any) *Definition

Attr declares a static attribute, mirroring `name "value"`.

func (*Definition) BeforeCreate

func (d *Definition) BeforeCreate(cb Callback) *Definition

BeforeCreate registers a before(:create) callback.

func (*Definition) Class

func (d *Definition) Class(class string) *Definition

Class sets the class name passed to the BuildFunc seam, mirroring `factory :user, class: "User"`. When unset it defaults to the factory name (or is inherited from the parent).

func (*Definition) Dynamic

func (d *Definition) Dynamic(name string, block Block) *Definition

Dynamic declares a lazily evaluated attribute, mirroring `name { ... }`. The block may read sibling attributes through the Evaluator.

func (*Definition) Factory

func (d *Definition) Factory(name string, fn func(f *Definition)) *Definition

Factory declares a nested (child) factory that inherits from the enclosing one, mirroring a `factory :admin do ... end` nested inside another factory.

func (*Definition) Parent

func (d *Definition) Parent(parent string) *Definition

Parent sets the parent factory to inherit attributes, traits, and callbacks from, mirroring `factory :admin, parent: :user`.

func (*Definition) Sequence

func (d *Definition) Sequence(name string, gen func(n int) any) *Definition

Sequence declares a per-factory sequence starting at 1, mirroring `sequence(:email) { |n| ... }`. Pass a nil generator to yield the raw number.

func (*Definition) SequenceFrom

func (d *Definition) SequenceFrom(name string, start int64, gen func(n int) any) *Definition

SequenceFrom is Definition.Sequence with an explicit starting value, mirroring `sequence(:email, 1000) { |n| ... }`.

func (*Definition) Trait

func (d *Definition) Trait(name string, fn func(t *Definition)) *Definition

Trait declares a named attribute overlay, mirroring `trait :admin do ... end`. A trait may itself declare attributes and callbacks; applying it overlays them at build time.

func (*Definition) Transient

func (d *Definition) Transient(fn func(t *Definition)) *Definition

Transient declares transient attributes, mirroring `transient do ... end`. They are available to other attributes and callbacks through the evaluator but are never passed to the BuildFunc seam.

type Evaluator

type Evaluator struct {
	// contains filtered or unexported fields
}

Evaluator resolves and memoizes a factory's attribute values during a build, mirroring factory_bot's evaluator. Dynamic attribute blocks and callbacks receive it and read sibling/transient attributes through Evaluator.Get.

func (*Evaluator) Get

func (e *Evaluator) Get(name string) any

Get returns the (memoized) value of the named attribute for use inside a dynamic block or callback, mirroring reading a sibling in factory_bot's evaluator. An unknown name yields nil. If resolving the attribute fails (for example a sequence overflow or an association cycle), Get panics with an internal marker that the build recovers and returns as an error.

type Opt

type Opt func(*callOptions)

Opt configures a Build/Create/AttributesFor call.

func With

func With(name string, value any) Opt

With overrides a single attribute, mirroring `build(:user, name: "x")`.

func WithAttrs

func WithAttrs(m map[string]any) Opt

WithAttrs overrides several attributes at once.

func WithTrait

func WithTrait(names ...string) Opt

WithTrait requests one or more traits, mirroring `build(:user, :admin)`.

type PersistFunc

type PersistFunc func(class string, obj any) error

PersistFunc is the persistence seam, mirroring factory_bot's to_create (default save!). It is invoked by Create/CreateList after the object is built and before(:create) callbacks have run. The default seam is a no-op.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds factory definitions and global sequences and constructs objects, mirroring FactoryBot's module-level registry.

func Default

func Default() *Registry

Default returns the process-wide default registry, the one a future rbgo binding maps the FactoryBot module onto.

func New

func New() *Registry

New returns an empty registry with default (map-returning / no-op) seams.

func (*Registry) AttributesFor

func (r *Registry) AttributesFor(name string, opts ...Opt) (map[string]any, error)

AttributesFor returns the resolved non-transient, non-association attributes without instantiating anything, mirroring `FactoryBot.attributes_for(:user, ...)`.

func (*Registry) Build

func (r *Registry) Build(name string, opts ...Opt) (any, error)

Build constructs an object without persisting it, mirroring `FactoryBot.build(:user, ...)`.

func (*Registry) BuildList

func (r *Registry) BuildList(name string, n int, opts ...Opt) ([]any, error)

BuildList builds n objects, mirroring `FactoryBot.build_list(:user, n, ...)`.

func (*Registry) Create

func (r *Registry) Create(name string, opts ...Opt) (any, error)

Create constructs and persists an object, mirroring `FactoryBot.create(:user, ...)`.

func (*Registry) CreateList

func (r *Registry) CreateList(name string, n int, opts ...Opt) ([]any, error)

CreateList creates n objects, mirroring `FactoryBot.create_list(:user, n, ...)`.

func (*Registry) Define

func (r *Registry) Define(name string, fn func(d *Definition)) error

Define registers a factory, mirroring `factory :name do ... end` inside `FactoryBot.define`. The block configures the factory through its Definition; nested factories are registered as children. It returns ErrDuplicateFactory if the name (or any nested name) is already registered.

func (*Registry) Generate

func (r *Registry) Generate(name string) (any, error)

Generate advances a global sequence and returns its value, mirroring `FactoryBot.generate(:email)`. It returns ErrUnknownSequence if no such sequence is registered.

func (*Registry) Sequence

func (r *Registry) Sequence(name string, gen func(n int) any)

Sequence registers a global sequence starting at 1, mirroring a top-level `sequence(:email) { |n| ... }`. Advance it with Registry.Generate.

func (*Registry) SequenceFrom

func (r *Registry) SequenceFrom(name string, start int64, gen func(n int) any)

SequenceFrom is Registry.Sequence with an explicit starting value.

func (*Registry) SetBuild

func (r *Registry) SetBuild(fn BuildFunc)

SetBuild installs the object-instantiation seam (see BuildFunc).

func (*Registry) SetPersist

func (r *Registry) SetPersist(fn PersistFunc)

SetPersist installs the persistence seam (see PersistFunc).

type Sequence

type Sequence struct {
	// contains filtered or unexported fields
}

Sequence is a monotonically increasing counter with an optional generator, mirroring factory_bot's sequence(:name) { |n| ... }. The counter starts at 1 by default (factory_bot's default) and is shared across every build that touches it, so successive objects receive successive values.

Jump to

Keyboard shortcuts

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