Skip to content

SOFware/foundries

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Foundries

Declarative trees of related records using factory_bot.

Foundries composes factory_bot factories into blueprints that know how to create, find, and relate records. You register blueprints with a base class, then build entire object graphs with a nested DSL:

TestFoundry.new do
  team "Engineering" do
    user "Alice"
    admin "Bob"

    project "API" do
      task "Auth", priority: "high"
      task "Caching"
    end
  end
end

Each method call creates a record (or finds an existing one), and nesting establishes parent-child context automatically. No manual foreign key wiring.

Installation

gem "foundries"

Usage

Blueprints

A blueprint wraps a single factory_bot factory and declares how it participates in the tree:

class TeamBlueprint < Foundries::Blueprint
  handles :team
  factory :team
  collection :teams
  parent :none
  permitted_attrs %i[name]

  def team(name, attrs = {}, &block)
    @attrs = attrs.merge(name: name)
    object = find(name) || create_object
    update_state_for_block(object, &block) if block
    object
  ensure
    reset_attrs
  end

  private

  def create_object
    create(:team, attrs).tap { |record| collection << record }
  end

  def attrs
    permitted_attrs @attrs
  end
end

Blueprint DSL

Method Purpose
handles :method_name Methods this blueprint exposes on the foundry
factory :name Which factory_bot factory to use (inferred from class name if omitted)
collection :name Collection name for tracking created records
parent :name How to find the parent record (:none, :self, or a method on current)
parent_key :foreign_key Foreign key column linking to the parent
permitted_attrs %i[...] Attributes allowed through to factory_bot
nested_attrs key => [...] For accepts_nested_attributes_for

Finding records

Blueprints automatically prevent duplicates. find(name) checks the in-memory collection first, then falls back to the database. find_by(criteria) works with arbitrary attributes.

Parent context

When a block is passed to a blueprint method, update_state_for_block saves the current context, sets the new record as current.resource, executes the block, then restores the previous context. Child blueprints read their parent from current:

class UserBlueprint < Foundries::Blueprint
  handles :user
  parent :team         # reads current.team
  parent_key :team_id  # sets team_id on created records
  # ...
end

Base (the foundry)

Register blueprints and optional extra collections:

class TestFoundry < Foundries::Base
  blueprint TeamBlueprint
  blueprint UserBlueprint
  blueprint ProjectBlueprint
  blueprint TaskBlueprint

  collection :tags  # extra collection not from a blueprint
end

The base class:

  • Instantiates each blueprint and delegates its handles methods
  • Initializes a Set for each collection (e.g. teams_collection)
  • Tracks current state so nested blocks know their parent context
  • Deduplicates records via each blueprint's find logic

Presets

Presets are named class methods that build a preconfigured foundry:

class TestFoundry < Foundries::Base
  # ...

  preset :dev_team do
    team "Engineering" do
      user "Alice"
      project "Main" do
        task "Setup"
      end
    end
  end
end

# In a test:
let(:foundry) { TestFoundry.dev_team }

Reopening

Add more records to an existing foundry:

foundry = TestFoundry.dev_team
foundry.reopen do
  team "Design" do
    user "Carol"
  end
end

Building from existing objects

Start from records already in the database:

foundry = TestFoundry.new
foundry.from(existing_team) do
  user "New hire"
end

Lifecycle hooks

Override setup and teardown in your base subclass for pre/post processing:

class TestFoundry < Foundries::Base
  private

  def setup
    @pending_rules = []
  end

  def teardown
    process_pending_rules
  end
end

Snapshot Caching

When using ActiveRecord, Foundries can snapshot preset data to disk and restore it instead of re-running factories. This is useful for speeding up test suites where the same preset is called many times.

Enable with an environment variable:

FOUNDRIES_CACHE=1 bundle exec rspec

Or configure directly:

Foundries::Snapshot.enabled = true
Foundries::Snapshot.storage_path = "tmp/foundries"  # default
Foundries::Snapshot.source_paths = [
  "lib/blueprints/**/*.rb",
  "lib/test_foundry.rb"
]

Snapshots are invalidated automatically when the schema version changes or when source files listed in source_paths are modified. Data is captured using database-native copy operations (PostgreSQL COPY, SQLite INSERT) and restored with referential integrity checks temporarily disabled.

Similarity Detection

Foundries can detect when presets have overlapping structure, highlighting consolidation opportunities. When enabled, it records the normalized blueprint call tree of each preset and compares against previously seen presets.

Enable with an environment variable:

FOUNDRIES_SIMILARITY=1 bundle exec rspec

Or configure directly:

Foundries::Similarity.enabled = true

When two presets share identical structure or one is structurally contained within another, a warning is printed to stderr:

[Foundries] Preset :basic and :extended have identical structure (team > [project > [task], user])
[Foundries] Preset :simple is structurally contained within :complex

Each unique pair is warned once per process. The detection normalizes trees by deduplicating sibling nodes (keeping the richest subtree), collapsing pass-through chains, and sorting alphabetically. This means presets that build the same shape of data are detected regardless of the specific names or attribute values used.

Requirements

  • Ruby >= 4.0
  • factory_bot >= 6.0
  • ActiveRecord (optional, for snapshot caching)

License

MIT

Development

After checking out the repo, run bundle install to install dependencies. Then, run bundle exec rake to run the tests.

To install this gem onto your local machine, run bundle exec rake install.

This project is managed with Reissue.

Releases are automated via the shared release workflow. Trigger a release by running the "Release gem to RubyGems.org" workflow from the Actions tab.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/SOFware/foundries.

About

Factory build related objects

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages