Changelog
View SourceAll notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.15.0] - 2026-09-24
Added
- Hook placements: a hook can be set as
{hook, :before_parent},{hook, :after_parent}, or{hook, :replace_parent}to run before, after, or instead of the hooks it inherits. - Hook names, duplicate hook names at one level, and hook values are validated at compile time. A hook must be a 1-arity remote capture; anything else, such as an anonymous function, raises.
Changed
- Breaking: A hook set at a lower level (child module or factory) is chained with the
inherited hooks of the same name instead of replacing them. The order is onion-style: a
parent's
before_*hooks run first and itsafter_*hooks run last. - Breaking:
__factory_man__(:opts)and__factory_man__(:opts, name)hold each hook name's resolved list of functions, in run order. - Hooks are compiled into the generated functions as direct calls, with no runtime lookup. A hook that points at a missing function is reported by a compile-time warning instead of failing at runtime.
[0.14.0] - 2026-09-23
Added
- Breaking: The
assocs:factory option replacesassociations:. It maps each association key to a builder: a 1-arity function (local or remote capture, or anonymous function), a 2-arity function that also receives the factory params with earlier keys resolved, or{builder, default: value}. Every declared key is resolved before the factory body, so the body always receives it resolved and a finalMap.merge(base_params, params)no longer restores the caller's raw input. The builder is the default: an absent key is built with%{}(or resolves to[]for a plural association) unlessdefault:says otherwise. defvariantacceptsassocs:. The variant resolves its keys before its body; the base factory reuses the resulting structs.assocs:is evaluated at build time, so it can hold local captures, anonymous functions, and shared declarations returned by a function call. It is validated when the factory first builds.assocs:checks that supplied structs and builder results are the association's schema. A builder for a singular association may returnnil.{builder, required: true}inassocs:makes a singular association resolve to a non-nil value: a caller'snil, or a builder that returnsnil, raises. An absent key still builds.- A recursion guard raises when a default build starts again inside itself (a self-referential
or mutually recursive association), instead of recursing forever. It covers
assocs:,assoc/3,4, andassoc_list/3,4, and the error shows the build path. assoc/3,4accepts a params map asdefault:, andassoc_list/3,4acceptsdefault:with a list of params maps.
Changed
- Breaking:
use FactoryMan,deffactory, anddefvariantraise on unknown options instead of ignoring them.
Removed
- Breaking: The
associations:option and its factory-name targets (:user,{Module, :user}). Useassocs:with function captures. - Breaking:
FactoryMan.resolve_assoc/2,3andFactoryMan.resolve_assoc_list/2,3. - Breaking: The
struct:andinherit:options ofassoc/3,4andassoc_list/3,4. Close over extra params in the builder instead:&build_user_struct(Map.merge(%{role: "writer"}, &1)).
[0.13.0] - 2026-09-19
Added
FactoryMan.assoc/3,4andFactoryMan.assoc_list/3,4read an association from a factory's params map by key. An absent key builds the default (assoc) or resolves to[](assoc_list);assoc/3,4acceptsdefault: nilfor an association that should only exist when the caller supplies one.FactoryMan.resolve_assoc/2,3andFactoryMan.resolve_assoc_list/2,3resolve a value or list directly, for helpers that hold the value instead of a params map.- Struct factories raise when given something other than a params map, instead of failing later inside the factory body.
Changed
- Breaking: An explicit
nilis preserved by the keyed helpers, the value helpers, and the declarative:associationsoption. The:on_niloption is removed; a factory that needs a record for a nil value provides the fallback in its own body. - Breaking:
assoc_listraises on a nil collection instead of resolving it to an empty list, matching declarative:many. Use[]for no associated values. - Breaking: The value-based
assoc/2,3andassoc_list/2,3from 0.12.0 are renamed toresolve_assocandresolve_assoc_list. Theassocnames now take a params map and a key. - Breaking: Lazy values are resolved in two passes: the 0-arity functions, then the 1-arity ones. A 1-arity function now receives resolved 0-arity values instead of function references.
- Breaking:
body: :structfactories lazily evaluate the struct their body returns. Function values in those fields were previously stored as-is. - Breaking:
:associationsis rejected as a module-level option. Association keys belong to one schema, so cascading them would apply a factory's keys to every struct in the module. - Association errors now name the failing key, and the item index for list associations.
- Documented the association tiers and their nil rules, when to reach for declarative versus
imperative resolution, and how to keep an imperatively resolved value through a factory body's
final
Map.merge/2.
0.12.1 - 2026-09-18
Changed
- Internal macro and build-pipeline helpers are hidden from the generated public API documentation.
0.12.0 - 2026-09-17
Added
- Declarative
associations:support for Ecto-backed factories. Association schemas and cardinality are derived from Ecto; same-module factories use atom references and cross-module factories use{FactoryModule, :factory}references. Caller-provided nested params are normalized into associated structs while missing keys continue to use factory defaults.
Changed
- Reworked the cookbook into a task-oriented, beginner-to-advanced guide with realistic test examples covering generated builders, sequences, lazy values, variants, associations, strict params, non-struct factories, inheritance, hooks, specialized construction, and debugging.
- Breaking:
FactoryMan.assoc/2,3andFactoryMan.assoc_list/2,3now resolve a value or list directly instead of extracting a value from a parent params map. The keyed interfaces andon_missing:option are removed. - The low-level helpers' optional
struct:check now validates builder results as well as supplied structs. Invalid builder results raiseArgumentError.
0.11.1 - 2026-09-15
Changed
- Documentation now distinguishes struct and non-struct factory function families, explains when each generated builder and insert function is useful, and documents multi-level factory inheritance and child option overrides.
- Sequence documentation now describes cycling lists,
:start_at, and reset behavior accurately.
Fixed
FactoryMan.sequence/3now has a formatter-function type spec that matches its implementation. Lists remain supported bysequence/2.
0.11.0 - 2026-07-03
Removed
- Breaking: Duplicate option warnings (and the
:suppress_duplicate_option_warningoption) are removed. The warning could only ever flag harmless same-value redundancy, yet failed builds under--warnings-as-errorsand needed its own suppression escape hatch. Redundancy linting belongs in consumer-side tooling. - Breaking: The compile-time tombstone errors for the options removed or renamed in 0.6.0
(
build_params?,build_struct?) are removed. Legacy keys are now ignored like any other unknown option.
0.10.0 - 2026-07-03
Changed
- Breaking:
use FactoryManno longer importsassoc/3,4andassoc_list/3,4. Helper functions are always called qualified (FactoryMan.assoc(...),FactoryMan.sequence(...)), making their origin explicit and avoiding collisions with generic names elsewhere (e.g.Ecto.assoc/2). Only the definition macrosdeffactory/defvariantremain imported — they read as DSL keywords. Documentation examples previously showedsequence(...)bare even though it was never imported; all examples now use the qualified form that actually compiles.
0.9.0 - 2026-07-03
Added
strict:option for struct factories:strict: trueraises anArgumentErrorwhen a caller passes param keys that are not fields of the:structoption's struct, catching typos at the factory boundary. Previously a typo surfaced late (struct!/2) for merge-style factories and never forbody: :structfactories, whose bodies read params selectively. Usestrict: [allow: [...]]to permit specific non-field keys (e.g. inputs used only to derive other fields). Cascades fromuse FactoryManlike other options; ignored for non-struct factories (matchingbody:). The check runs at build entry, so params builders, inserts, lists, and variants are all covered.
0.8.0 - 2026-07-03
Added
assoc/4accepts an:on_missingoption::build(default) keeps the existing behavior of building the default association;nilmakes a missing key resolve tonil, for associations that should only exist when the caller supplies one. Independent of:on_nil; typically paired withon_nil: :keep.
0.7.0 - 2026-07-02
Added
insert_<name>_struct/1,2: inserts an already-built struct through the factory's insert pipeline (before_inserthook, repo insert with options,after_inserthook). Closes a consistency hole: modifying a built struct and callingRepo.insert!/2directly skips the factory's insert hooks, producing records shaped differently frominsert_*results. Variants delegate to their base factory's pipeline. Generated under the same conditions as the other insert functions.__factory_man__(:factories)reflection: lists every factory and variant name registered in a module (variants under their full name), in definition order. Enables runtime dispatch — selecting and calling factories by name — without string-building function names.FactoryMan.assoc/4andFactoryMan.assoc_list/4(auto-imported byuse FactoryMan): resolve association values from factory params. A missing key builds a default, a struct is reused (type-checked against the:structoption, which raises on a mismatch), and a params map builds the association from those params (merged over:inheritdefaults).assoc_list/4applies the same rules per element;on_nil: :keepsupports optional associations. Replaces the hand-writtencase/Map.get_lazypatterns previously shown in the docs, which silently misbehaved when given a struct of the wrong type.
0.6.0 - 2026-07-02
Added
- Generated factory functions now carry
@docattributes, so they show up documented in HexDocs, IExh/1, and editor tooltips instead of appearing undocumented. - Variants are now registered under their full name, so a variant can itself be used as the
base of another variant (e.g.
defvariant senior(params \\ %{}), for: :admin_user). Previously this raised "base factory not found" at compile time.
Changed
- Breaking: The
build_params?option is renamed tobody, with values:params(default) and:struct(the factory body returns a struct directly; formerlybuild_params?: false). Since the params unification, params functions are always generated, so the old name's "generate params builders?" reading had become misleading — the option only controls what the factory body returns. The old key raises a compile-timeArgumentError, as does an unrecognizedbodyvalue. - Breaking:
params_for_*andstring_params_for_*are renamed tobuild_*_paramsandbuild_*_string_params, replacing the former raw params builders. For struct factories,build_*_paramsnow builds the struct and converts it to a clean map (Ecto metadata stripped for schemas,Map.from_struct/1for plain structs) instead of returning the factory body's raw output. The raw params stage still exists insidebuild_*_struct(hooks and lazy evaluation are unchanged) but is no longer a public function. Consequences:- Factory bodies of struct factories must return only struct fields (always passed through
struct!/2now). build_params?: falsefactories now also getbuild_*_params(derived from the struct).build_*_string_params_listvariants are generated (previouslystring_params_for_*had no list variant).- Non-struct factories are unchanged (
build_*still returns the body's value verbatim).
- Factory bodies of struct factories must return only struct fields (always passed through
- Breaking: The
build_struct?option is removed and now raises a compile-timeArgumentError. Params functions are derived from the built struct, so "params-only" struct factories are no longer expressible — omit the:structoption instead. - Breaking: The debug functions
_factory_opts/0and_<name>_factory_opts/0are replaced by a single reflection function following the Elixir dunder convention (like__schema__):__factory_man__(:opts)for module options and__factory_man__(:opts, factory_name)for a factory's (or variant's) merged options. - Internal refactor:
deffactoryanddefvariantnow generate their shared function families (list builders,params_for_*/string_params_for_*, insert convenience/list functions) from common templates in an internal codegen module, removing ~200 lines of drifted duplication. - Breaking (edge case): variant list convenience functions (
build_<variant>_list/1andbuild_<variant>_params_list/1) are now generated under the same conditions as theirdeffactorycounterparts — when the factory head has a default argument — and call the item builder with its actual default instead of always passing%{}. Variants of factories whose argument has no default no longer get the 1-arity list convenience. - Duplicate option warnings are now emitted with
IO.warninstead ofLogger.warning, so they carry file/line attribution and are caught by--warnings-as-errors. - Test suite cleanup: removed tests made redundant by the params unification, retitled the
stale "paramsfor" section, and added coverage for `build*_string_params_list
, embedded schema params, and inheritedafter_insert` hooks running on child-module inserts. - Documentation restructured: the README is now a short onboarding tour (installation, quick
tour, how it works, which function to use, project structure), and the
FactoryManmoduledoc is the full reference — ending the near-total duplication between the two. Hooks and variants moved up in the reference; added a mermaid diagram of the generated-function pipeline and a cookbook section (building associations). The README install snippet now recommendsonly: [:dev, :test].
Fixed
An unescaped interpolation in the moduledoc's hooks example baked a compile-time timestamp into the published docs; the example now renders
System.os_time()literally as intended.Variant list builders no longer crash for variants of non-struct factories with non-map defaults (e.g.
defvariant loud(name \\ "world"), for: :greeting). Previouslybuild_loud_greeting_list(2)passed%{}to the variant body.Module-level hooks now merge per hook key across
extends:, as documented. Previously a child module that set anyhooks:option replaced the parent module's hooks wholesale, silently dropping parent hooks for other keys.Helper functions are now actually inherited via
extends:, as documented. Child factory modules import all public functions from the full ancestor chain, so helpers likegenerate_username()can be called unqualified. Previously this only worked with explicit qualification (e.g.MyApp.Factory.generate_username()) despite the documentation showing otherwise. Note: a child module that defines a factory with the same name as one in a parent module will now get a compile-time import conflict error.
0.5.0 - 2026-06-15
Changed
- Breaking: Generated insert functions no longer include a trailing
!. For example,insert_user!/0,1,2is nowinsert_user/0,1,2andinsert_user_list!/1,2,3is nowinsert_user_list/1,2,3. This aligns the API with other factory libraries and removes the implication that a non-bang variant exists.
0.4.1 - 2026-03-11
Fixed
build_params?: falseno longer raises when used on non-struct factories (or inherited from a parent module). Non-struct factories always generate theirbuild_*functions regardless of this option. Previously, the compile-time validation predated barebones factories and incorrectly rejected this combination.
0.4.0 - 2026-03-10
Changed
- Breaking: Non-struct factories now generate
build_*/0,1andbuild_*_list/1,2instead ofbuild_*_params/0,1andbuild_*_params_list/1,2. The_paramssuffix was misleading for factories that can return any value. Struct factories are unchanged.
0.3.2 - 2026-03-10
Added
params_for_*andstring_params_for_*functions for Ecto schema factories. These build a struct then strip Ecto metadata (__meta__, autogenerated IDs,NotLoadedassociations,belongs_tostructs), returning a clean map for changesets or controller tests. Foreign keys are set automatically for persistedbelongs_toassociations.- Unlike ExMachina's
params_for, nil values are preserved (not silently dropped) andstring_params_forleaves struct values likeDateTimeuntouched (not converted to maps).
0.3.1 - 2026-03-10
Added
- Factory bodies can now return arbitrary values (strings, keyword lists, tuples, nil, etc.),
not just maps. Factories without
:structare no longer restricted to returning maps. - Lazy evaluation now works in keyword lists — 0-arity and 1-arity function values are resolved at build time, matching the existing behavior for maps and structs.
Fixed
build_*_params_list/1(single-arity convenience) now calls the factory with its actual default argument instead of always passing%{}. Previously, factories with non-map defaults would crash when using the list builder without explicit arguments.
0.3.0 - 2026-03-10
Changed
- Breaking: Renamed
params?option tobuild_params?for consistency withbuild_struct?
0.2.2 - 2026-03-10
Added
- Duplicate option warnings: FactoryMan now emits a compile-time
Logger.warningwhen a child factory module ordeffactoryspecifies an option that is already defined by the parent with the same value. Helps catch redundant copy-pasted options. suppress_duplicate_option_warning: trueoption to silence the warning at module or factory level when the duplication is intentional. This option does not propagate to child modules.
0.2.1 - 2026-03-09
Added
:asoption fordefvariantto customize the generated function name. By default, variant functions are named{variant}_{base}(e.g.build_admin_user_struct). The:asoption overrides this combined name (e.g.as: :modgeneratesbuild_mod_structinstead ofbuild_moderator_user_struct).
0.2.0 - 2026-03-08
Added
build_params?: falseoption fordeffactory. When set, the factory body returns a struct directly instead of a params map. Nobuild_*_paramsfunctions are generated. Useful for complex factories that need full control over struct construction (e.g. resolving associations from other factories, conditional logic). Can be set at module level or factory level.defvariantmacro for defining variant factories that wrap a base factory. The variant body is a preprocessor: it transforms caller params before delegating to the base factory. Generates the full set of named functions (e.g.build_admin_user_struct/0,1,insert_admin_user!/0,1,2).- Compile-time validation:
build_params?: falsewithoutstruct:raisesArgumentError - Compile-time validation:
defvariantreferencing undefined base factory raisesArgumentError
Fixed
- Flaky "circular sequence cycles through values" test that depended on test ordering. Added
FactoryMan.Sequence.reset()to ensure predictable starting position.
0.1.1 - 2026-03-07
Fixed
- Factory-level hooks were being flattened into top-level options instead of staying nested under
the
:hookskey. This caused_factory_opts()and_<name>_factory_opts()debug functions to return a polluted keyword list with hook keys (e.g.:after_insert) mixed in alongside configuration keys (e.g.:repo,:struct). Hooks now stay properly nested. - Fixed several inaccurate examples in moduledoc and README (e.g.
build_api_payload()corrected tobuild_api_payload_params(), missingMap.mergecalls in examples)
Changed
- Replaced
List.pop_atwithEnum.atinFactoryMan.Sequencefor list-based sequences (avoids constructing an unused remainder list) - Reorganized demo factory definitions into logical sections: core, lazy evaluation, sequences, factory options, and parameter patterns
- Removed redundant demo factories (
params_only,with_custom_param_name) and renamedwith_after_build_params_hooktohooked - Refactored test suite: reorganized into
describeblocks by feature, removed ~21 redundant tests, fixed misleading test names and broken assertions. 69 tests remain (was 90), all meaningful. - Added Dialyzer configuration (
plt_add_apps: [:ex_unit]). Zero warnings. - Expanded hooks documentation with pipeline diagram, reference table, precedence rules, and practical examples
- Added lazy evaluation ordering warning explaining that 1-arity lazy functions receive the pre-evaluation map
- Rewrote AGENTS.md with usage rules, canonical patterns, and anti-patterns
0.1.0 - 2026-02-08
Added
- Initial alpha release
deffactorymacro for defining factories- Automatic struct building with
build_*_structfunctions - Database insertion with
insert_*!functions - Params-only factories without database dependency (i.e. only has
build_*_paramsandbuild_*_params_list) - Sequence generation for unique values
- Lazy evaluation for computed attributes
- Factory inheritance via
:extendsoption - Hooks system for custom transformations
- List factories for bulk data creation (
*_listvariants) - Support for embedded schemas (Build struct, but do not attempt to generate
insert_*functions)