Skip to content

Commit

Permalink
Extract all Ecto functionality to separate module
Browse files Browse the repository at this point in the history
This should make it easier to add other adapters in the future and
reduces the need for conditionals due to separation of concerns.
  • Loading branch information
jsteiner committed Sep 16, 2015
1 parent b57463f commit 270c19b
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 269 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ defp app_list, do: [:logger]
defmodule MyApp.Factories do
# MyApp.Repo is an Ecto Repo.
# It will automatically be used when calling `create`
use ExMachina, repo: MyApp.Repo
use ExMachina.Ecto, repo: MyApp.Repo

def factory(:config) do
# Factories can be plain maps
Expand Down Expand Up @@ -83,7 +83,6 @@ defining `save_function/1` in your module.

```elixir
defmodule MyApp.JsonFactories do
# Note `repo` was not passed as an option
use ExMachina

def factory(:user), do: %User{name: "John"}
Expand All @@ -104,7 +103,7 @@ or `create_json` to return encoded JSON objects.

```elixir
defmodule MyApp.Factories do
use ExMachina, repo: MyApp.Repo
use ExMachina.Ecto, repo: MyApp.Repo

def factory(:user), do: %User{name: "John"}

Expand Down
175 changes: 43 additions & 132 deletions lib/ex_machina.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,15 @@ defmodule ExMachina do

defmodule UndefinedSave do
@moduledoc """
Error raised when trying to call create and no repo or save_function is
defined.
Error raised when trying to call create and save_record/1 is
not defined.
"""

defexception [:message]

def exception do
%UndefinedSave{
message: "Define save_function/1 or include the repo option. See docs
for ExMachina.save_record."
message: "Define save_record/1. See docs for ExMachina.save_record/1."
}
end
end
Expand All @@ -38,21 +37,12 @@ defmodule ExMachina do

def start(_type, _args), do: ExMachina.Sequence.start_link

defmacro __using__(opts) do
defmacro __using__(_opts) do
quote do
@before_compile unquote(__MODULE__)
@repo Dict.get(unquote(opts), :repo)

import ExMachina, only: [sequence: 2]

defp assoc(attrs, factory_name, opts \\ []) do
ExMachina.assoc(__MODULE__, attrs, factory_name, opts)
end

def fields_for(factory_name, attrs \\ %{}) do
ExMachina.fields_for(__MODULE__, factory_name, attrs)
end

def build(factory_name, attrs \\ %{}) do
ExMachina.build(__MODULE__, factory_name, attrs)
end
Expand All @@ -68,10 +58,6 @@ defmodule ExMachina do
def create_list(number_of_factories, factory_name, attrs \\ %{}) do
ExMachina.create_list(__MODULE__, number_of_factories, factory_name, attrs)
end

def save_record(record) do
ExMachina.save_record(__MODULE__, @repo, record)
end
end
end

Expand All @@ -89,69 +75,6 @@ defmodule ExMachina do
"""
def sequence(name, formatter), do: ExMachina.Sequence.next(name, formatter)

@doc """
Gets a factory from the passed in attrs, or creates if none is present
## Examples
attrs = %{user: %{name: "Someone"}}
# Returns attrs.user
assoc(attrs, :user)
attrs = %{}
# Creates and returns new instance based on :user factory
assoc(attrs, :user)
attrs = %{}
# Creates and returns new instance based on :user factory
assoc(attrs, :author, factory: :user)
"""
def assoc(module, attrs, factory_name, opts \\ []) do
case Map.get(attrs, factory_name) do
nil -> create_assoc(module, factory_name, opts)
record -> record
end
end

defp create_assoc(module, _factory_name, factory: factory_name) do
ExMachina.create(module, factory_name)
end
defp create_assoc(module, factory_name, _opts) do
ExMachina.create(module, factory_name)
end

@doc """
Builds a factory with the passed in factory_name and returns its fields
This is only for use with Ecto models.
Will return a map with the fields and virtual fields, but without the Ecto
metadata and associations.
## Example
def factory(:user) do
%MyApp.User{name: "John Doe", admin: false}
end
# Returns %{name: "John Doe", admin: true}
fields_for(:user, admin: true)
"""
def fields_for(module, factory_name, attrs \\ %{}) do
module.build(factory_name, attrs)
|> drop_ecto_fields
end

defp drop_ecto_fields(record = %{__struct__: struct, __meta__: %{__struct__: Ecto.Schema.Metadata}}) do
record
|> Map.from_struct
|> Map.delete(:__meta__)
|> Map.drop(struct.__schema__(:associations))
end
defp drop_ecto_fields(record) do
raise ArgumentError, "#{inspect record} is not an Ecto model. Use `build` instead."
end

@doc """
Builds a factory with the passed in factory_name
Expand All @@ -172,10 +95,11 @@ defmodule ExMachina do
@doc """
Builds and saves a factory with the passed in factory_name
If you pass in repo when using ExMachina it will use the Ecto Repo to save the
record automatically. If you do not pass the repo, you need to define a
`save_record/1` function in your module. See `save_record` docs for more
information.
If using ExMachina.Ecto it will use the Ecto Repo passed in to save the
record automatically.
If you are not using ExMachina.Ecto, you need to define a `save_record/1`
function in your module. See `save_record` docs for more information.
## Example
Expand All @@ -190,50 +114,6 @@ defmodule ExMachina do
ExMachina.build(module, factory_name, attrs) |> module.save_record
end


@doc """
Saves a record when `create` is called. Uses Ecto if the `repo` option is set
If you include the `repo` option (`use ExMachina, repo: MyApp.Repo`) this
function will call `insert!` on the passed in repo.
If you do not pass in the `repo` option, you must define a custom
save_function/1 for saving the record.
## Examples
defmodule MyApp.Factories do
use ExMachina, repo: MyApp.Repo
def factory(:user), do: %User{name: "John"}
end
# Will build and save the record to the MyApp.Repo
MyApp.Factories.create(:user)
defmodule MyApp.JsonFactories do
# Note `repo` was not passed as an option
use ExMachina
def factory(:user), do: %User{name: "John"}
def save_function(record) do
# Poison is a library for working with JSON
Poison.encode!(record)
end
end
# Will build and then return a JSON encoded version of the map
MyApp.JsonFactories.create(:user)
"""
def save_record(module, repo, record) do
if repo do
repo.insert!(record)
else
module.save_function(record)
end
end

@doc """
Creates and returns 2 records with the passed in factory_name and attrs
Expand Down Expand Up @@ -279,10 +159,41 @@ defmodule ExMachina do
end

@doc """
Raises a helpful error if `create` is called and no save_function is
defined.
Saves a record when `create` is called. Uses Ecto if using ExMachina.Ecto
If using ExMachina.Ecto (`use ExMachina.Ecto, repo: MyApp.Repo`) this
function will call `insert!` on the passed in repo.
If you are not using ExMachina.Ecto, you must define a custom
save_record/1 for saving the record.
## Examples
defmodule MyApp.Factories do
use ExMachina.Ecto, repo: MyApp.Repo
def factory(:user), do: %User{name: "John"}
end
# Will build and save the record to the MyApp.Repo
MyApp.Factories.create(:user)
defmodule MyApp.JsonFactories do
# Note, we are not using ExMachina.Ecto
use ExMachina
def factory(:user), do: %User{name: "John"}
def save_function(record) do
# Poison is a library for working with JSON
Poison.encode!(record)
end
end
# Will build and then return a JSON encoded version of the map
MyApp.JsonFactories.create(:user)
"""
def save_function(record) do
def save_record(record) do
raise UndefinedSave
end
end
Expand Down
93 changes: 93 additions & 0 deletions lib/ex_machina/ecto.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
defmodule ExMachina.Ecto do
defmacro __using__(opts) do
quote do
use ExMachina

@repo Dict.fetch!(unquote(opts), :repo)

def fields_for(factory_name, attrs \\ %{}) do
ExMachina.Ecto.fields_for(__MODULE__, factory_name, attrs)
end

defp assoc(attrs, factory_name, opts \\ []) do
ExMachina.Ecto.assoc(__MODULE__, attrs, factory_name, opts)
end

def save_record(record) do
ExMachina.Ecto.save_record(__MODULE__, @repo, record)
end
end
end

@doc """
Builds a factory with the passed in factory_name and returns its fields
This is only for use with Ecto models.
Will return a map with the fields and virtual fields, but without the Ecto
metadata and associations.
## Example
def factory(:user) do
%MyApp.User{name: "John Doe", admin: false}
end
# Returns %{name: "John Doe", admin: true}
fields_for(:user, admin: true)
"""
def fields_for(module, factory_name, attrs \\ %{}) do
module.build(factory_name, attrs)
|> drop_ecto_fields
end

defp drop_ecto_fields(record = %{__struct__: struct, __meta__: %{__struct__: Ecto.Schema.Metadata}}) do
record
|> Map.from_struct
|> Map.delete(:__meta__)
|> Map.drop(struct.__schema__(:associations))
end
defp drop_ecto_fields(record) do
raise ArgumentError, "#{inspect record} is not an Ecto model. Use `build` instead."
end

@doc """
Gets a factory from the passed in attrs, or creates if none is present
## Examples
attrs = %{user: %{name: "Someone"}}
# Returns attrs.user
assoc(attrs, :user)
attrs = %{}
# Creates and returns new instance based on :user factory
assoc(attrs, :user)
attrs = %{}
# Creates and returns new instance based on :user factory
assoc(attrs, :author, factory: :user)
"""
def assoc(module, attrs, factory_name, opts \\ []) do
case Map.get(attrs, factory_name) do
nil -> create_assoc(module, factory_name, opts)
record -> record
end
end

defp create_assoc(module, _factory_name, factory: factory_name) do
ExMachina.create(module, factory_name)
end
defp create_assoc(module, factory_name, _opts) do
ExMachina.create(module, factory_name)
end

@doc """
Saves a record using `Repo.insert!` when `create` is called.
"""
def save_record(module, repo, record) do
if repo do
repo.insert!(record)
end
end
end
Loading

0 comments on commit 270c19b

Please sign in to comment.