-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rename to elixir-dx/refactory and traits (was infer-beam/refinery and…
… refinements)
- Loading branch information
1 parent
7216854
commit 68f7177
Showing
23 changed files
with
463 additions
and
421 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,127 @@ | ||
# Refinery | ||
# Refactory | ||
|
||
An Elixir library to generate test data recursively with refinements | ||
An Elixir library to generate test data recursively with traits | ||
|
||
## Installation | ||
|
||
The package can be installed | ||
by adding `refinery` to your list of dependencies in `mix.exs`: | ||
by adding `refactory` to your list of dependencies in `mix.exs`: | ||
|
||
```elixir | ||
def deps do | ||
[ | ||
{:refinery, "~> 0.1.0", github: "infer-beam/refinery", only: :test} | ||
{:refactory, "~> 0.1.0", only: :test} | ||
] | ||
end | ||
``` | ||
|
||
Documentation can found at <https://infer-beam.github.io/refinery/>. | ||
Documentation can found at <https://hexdocs.pm/refactory/>. | ||
|
||
<!-- MODULEDOC --> | ||
Refactory allows generating Ecto records with nested overrides for your tests. | ||
|
||
## Factory module | ||
|
||
To start using Refactory, first define a factory module: | ||
|
||
``` | ||
defmodule MyApp.Factory do | ||
use Refactory, repo: MyApp.Repo | ||
end | ||
``` | ||
|
||
## Usage | ||
|
||
The factory module has two functions: | ||
|
||
- `build/2` generates an Ecto record with the given traits applied | ||
- `create/2` inserts an Ecto record into the database | ||
|
||
## Traits | ||
|
||
A trait can be | ||
- a `Map` in which each key-value pair is either | ||
- a field with its value | ||
- an association with a trait (for `belongs_to`, `has_one`, and `embeds_one`) | ||
- _soon:_ an association with a list of traits (for `has_many` and `embeds_many`) | ||
- a custom trait defined in the factory module (see below) | ||
- a `Tuple` with multiple traits to be applied | ||
|
||
## Basic example | ||
|
||
``` | ||
defmodule MyApp.Factory do | ||
use Refactory, repo: MyApp.Repo | ||
end | ||
MyApp.Factory.build(MyApp.List, %{ | ||
title: "Refined List", | ||
created_by_user: %{email: "test@email.org"} | ||
}) | ||
%MyApp.List{ | ||
title: "Refined List", | ||
created_by_user: %MyApp.User{ | ||
email: "test@email.org" | ||
} | ||
} | ||
``` | ||
|
||
## Default traits | ||
|
||
Default traits can be defined in the factory module. | ||
They are always applied first. | ||
|
||
``` | ||
defmodule MyApp.Factory do | ||
use Refactory, repo: MyApp.Repo | ||
def trait(MyApp.List, :default) do | ||
%{ | ||
title: "Default Title" | ||
} | ||
end | ||
end | ||
MyApp.Factory.build(MyApp.List) | ||
%MyApp.List{title: "Default Title"} | ||
``` | ||
|
||
## Custom traits | ||
|
||
Custom traits can be defined in the factory module and then used by their name. | ||
|
||
``` | ||
defmodule MyApp.Factory do | ||
use Refactory, repo: MyApp.Repo | ||
def trait(MyApp.List, :default) do | ||
%{ | ||
title: "Default Title" | ||
} | ||
end | ||
def trait(MyApp.List, :with_admin_user) do | ||
%{ | ||
created_by_user: %{ | ||
role: :admin | ||
} | ||
} | ||
end | ||
end | ||
MyApp.Factory.build(MyApp.List, :with_admin_user) | ||
%MyApp.List{title: "Default Title", created_by_user: %MyApp.User{role: :admin}} | ||
``` | ||
|
||
<!-- MODULEDOC --> | ||
|
||
## Special thanks | ||
|
||
This project is sponsored and kindly supported by [Team Engine](https://www.teamengine.co.uk/). | ||
|
||
If you'd like to join us working on [Infer](https://github.com/infer-beam/infer) and Refinery as a contractor, please [reach out](https://tinyurl.com/engine-infer-dev2). | ||
If you'd like to join us working on [Dx](https://github.com/elixir-dx/dx) and Refactory as a contractor, please reach out to @arnodirlam. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,15 @@ | ||
import Config | ||
|
||
config :refinery, Refinery.Test.Repo, | ||
config :refactory, Refactory.Test.Repo, | ||
hostname: "localhost", | ||
username: "postgres", | ||
password: "postgres", | ||
database: "refinery_test", | ||
pool: Ecto.Adapters.SQL.Sandbox, | ||
priv: "test/schema" | ||
|
||
config :refinery, | ||
ecto_repos: [Refinery.Test.Repo], | ||
repo: Refinery.Test.Repo | ||
config :refactory, | ||
ecto_repos: [Refactory.Test.Repo], | ||
repo: Refactory.Test.Repo | ||
|
||
config :logger, level: :warn |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
defmodule Refactory do | ||
@external_resource Path.expand("./README.md") | ||
@moduledoc File.read!(Path.expand("./README.md")) | ||
|> String.split("<!-- MODULEDOC -->") | ||
|> Enum.at(1) | ||
|
||
defmacro __using__(opts) do | ||
quote location: :keep do | ||
def refinery_repo() do | ||
unquote(opts[:repo]) | ||
end | ||
|
||
def create(type, traits \\ %{}) do | ||
Refactory.create(__MODULE__, type, traits) | ||
end | ||
|
||
def build(type, traits \\ %{}) do | ||
Refactory.build(__MODULE__, type, traits) | ||
end | ||
end | ||
end | ||
|
||
@doc """ | ||
Inserts an Ecto record with the given traits applied into the database | ||
""" | ||
def create(module, type, traits \\ %{}) do | ||
repo = module.refinery_repo() | ||
build(module, type, traits) |> repo.insert!() | ||
end | ||
|
||
@doc """ | ||
Generates an Ecto record with the given traits applied | ||
""" | ||
def build(module, type, traits \\ %{}) do | ||
case resolve_refinement(module, type, {:default, traits}) do | ||
record = %{__struct__: ^module} -> | ||
record | ||
|
||
record = %{__struct__: _module} -> | ||
raise ArgumentError, "Expected a struct of type #{module}. Got #{inspect(record)}" | ||
|
||
attrs -> | ||
do_build(module, type, attrs) | ||
end | ||
end | ||
|
||
defp do_build(module, type, attrs) do | ||
record = struct!(type, attrs) | ||
|
||
# set associations in record | ||
Enum.reduce(attrs, record, fn {name, traits}, record -> | ||
assoc = | ||
case ecto_association(type, name) || ecto_embed(type, name) do | ||
%Ecto.Association.BelongsTo{related: type} -> {:build_one, type} | ||
%Ecto.Association.Has{cardinality: :one, related: type} -> {:build_one, type} | ||
%Ecto.Embedded{cardinality: :one, related: type} -> {:build_one, type} | ||
_ -> :skip | ||
end | ||
|
||
case {assoc, traits} do | ||
{{:build_one, type}, %type{}} -> | ||
record | ||
|
||
{{:build_one, type}, %other_type{}} -> | ||
raise ArgumentError, | ||
"Expected value of type #{type} for #{type}.#{name}. Got #{other_type}" | ||
|
||
{{:build_one, _type}, nil} -> | ||
Map.put(record, name, nil) | ||
|
||
{{:build_one, type}, _} -> | ||
associated_record = build(module, type, traits) | ||
Map.put(record, name, associated_record) | ||
|
||
{:skip, _} -> | ||
record | ||
end | ||
end) | ||
end | ||
|
||
defp resolve_refinement(module, type, :default) do | ||
module.trait(type, :default) | ||
rescue | ||
_e in [UndefinedFunctionError, FunctionClauseError] -> %{} | ||
e -> reraise e, __STACKTRACE__ | ||
end | ||
|
||
defp resolve_refinement(module, type, trait) do | ||
module.trait(type, trait) | ||
rescue | ||
_e in [UndefinedFunctionError, FunctionClauseError] -> | ||
merge_refinements(module, type, trait, %{}) | ||
|
||
e -> | ||
reraise e, __STACKTRACE__ | ||
end | ||
|
||
defp merge_refinements(module, type, traits, result) when is_tuple(traits) do | ||
traits | ||
|> Tuple.to_list() | ||
|> Enum.reduce(result, &deep_merge(&2, resolve_refinement(module, type, &1), false, true)) | ||
end | ||
|
||
defp merge_refinements(_module, _type, traits, result) when is_map(traits) do | ||
deep_merge(result, traits, false) | ||
end | ||
|
||
defp merge_refinements(module, type, trait, _result) do | ||
raise ArgumentError, "Unknown trait for #{type} in #{module}: #{inspect(trait)}" | ||
end | ||
|
||
defp ecto_association(type, name), do: type.__schema__(:association, name) | ||
defp ecto_embed(type, name), do: type.__schema__(:embed, name) | ||
|
||
defp deep_merge(left, right, concat_lists? \\ true, struct_overrides? \\ false) do | ||
Map.merge(left, right, &deep_resolve(&1, &2, &3, concat_lists?, struct_overrides?)) | ||
end | ||
|
||
defp deep_resolve(_key, _left, %{__struct__: _type} = right, _concat_lists?, true) do | ||
right | ||
end | ||
|
||
defp deep_resolve( | ||
_key, | ||
%{__struct__: type} = left, | ||
%{__struct__: type} = right, | ||
_concat_lists?, | ||
_struct_overrides? | ||
) do | ||
struct!(type, deep_merge(Map.from_struct(left), Map.from_struct(right))) | ||
end | ||
|
||
defp deep_resolve(_key, %{__struct__: type}, _right, _concat_lists?, _struct_overrides?) do | ||
raise ArgumentError, "#{type} cannot be merged with non-#{type}." | ||
end | ||
|
||
defp deep_resolve(_key, _left, %{__struct__: type}, _concat_lists?, _struct_overrides?) do | ||
raise ArgumentError, "Non-#{type} cannot be merged with #{type}." | ||
end | ||
|
||
defp deep_resolve(_key, %{} = left, %{} = right, _concat_lists?, _struct_overrides?) do | ||
deep_merge(left, right) | ||
end | ||
|
||
defp deep_resolve(_key, left, right, true, _struct_overrides?) | ||
when is_list(left) and is_list(right) do | ||
left ++ right | ||
end | ||
|
||
defp deep_resolve(_key, _left, right, _concat_lists?, _struct_overrides?), do: right | ||
end |
Oops, something went wrong.