Skip to content

Commit

Permalink
feat: factory association builder
Browse files Browse the repository at this point in the history
  • Loading branch information
cylkdev committed Oct 16, 2022
1 parent 5e86602 commit 1c3e75c
Show file tree
Hide file tree
Showing 19 changed files with 887 additions and 247 deletions.
204 changes: 66 additions & 138 deletions lib/factory_ex.ex
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
defmodule FactoryEx do
@build_definition [
keys: [
type: {:in, [:atom, :string, :camel_string]},
doc: "Sets the type of keys to have in the built object, can be one of `:atom`, `:string` or `:camel_string`"
]
]

@moduledoc """
#{File.read!("./README.md")}
### FactoryEx.build options
We can also specify options to `&FactoryEx.build/3`
#{NimbleOptions.docs(@build_definition)}
#{NimbleOptions.docs(FactoryEx.OptionsValidator.schema())}
"""

@typedoc """
Build Schema Options
"""
@type build_schema_options :: [
keys: :atom | :string | :camel_string,
only: list() | map(),
except: list() | map()
]

alias FactoryEx.Utils
@typedoc """
Association Param Build Options
"""
@type build_assoc_options :: [
relational: :all | :atom
]

@type build_opts :: [
keys: :atom | :string | :camel_string
@typedoc """
Schema Factory Build Options
"""
@type build_schema_factory_options :: [
compile_schema_factory?: true | false,
factories: [module(), ...],
paths: [String.t(), ...],
schema_factory: map()
]

@typedoc """
Build options for the Schema, Association, or Schema Factory
"""
@type build_options() :: build_schema_options() | build_assoc_options() | build_schema_factory_options()

@doc """
Callback that returns the schema module.
"""
Expand All @@ -44,153 +62,63 @@ defmodule FactoryEx do
@optional_callbacks [build_struct: 1]

@doc """
Builds many parameters for a schema `changeset/2` function given the factory
`module` and an optional list/map of `params`.
"""
@spec build_many_params(pos_integer, module()) :: [map()]
@spec build_many_params(pos_integer, module(), keyword() | map()) :: [map()]
@spec build_many_params(pos_integer, module(), keyword() | map(), build_opts) :: [map()]
def build_many_params(count, module, params \\ %{}, opts \\ []) do
Enum.map(1..count, fn _ -> build_params(module, params, opts) end)
end

@doc """
Builds the parameters for a schema `changeset/2` function given the factory
`module` and an optional list/map of `params`.
Builds paramters and associations for a schema `changeset/2` function
given the factory `module` and an optional list/map of `params`.
"""
@spec build_params(module()) :: map()
@spec build_params(module(), keyword() | map()) :: map()
@spec build_params(module(), keyword() | map(), build_opts) :: map()
def build_params(module, params \\ %{}, opts \\ [])

def build_params(module, params, opts) when is_list(params) do
build_params(module, Map.new(params), opts)
@spec build_params(module(), keyword() | map(), build_options()) :: map()
def build_params(factory_module, params \\ %{}, options \\ [])
def build_params(factory_module, params, options) when is_map(params) do
options = FactoryEx.OptionsValidator.validate!(options)

factory_module.schema()
|> FactoryEx.SchemaAssociations.build_params(params, options)
|> then(&FactoryEx.SchemaBuilder.build_params(factory_module, &1, options))
end

def build_params(module, params, opts) do
opts = NimbleOptions.validate!(opts, @build_definition)

params
|> module.build()
|> Utils.deep_struct_to_map()
|> maybe_encode_keys(opts)
end

defp maybe_encode_keys(params, []), do: params

defp maybe_encode_keys(params, opts) do
case opts[:keys] do
nil -> params
:atom -> params
:string -> Utils.stringify_keys(params)
:camel_string -> Utils.camelize_keys(params)
end
end

@spec build_invalid_params(module()) :: map()
def build_invalid_params(module) do
params = build_params(module)
schema = module.schema()

field = schema.__schema__(:fields)
|> Kernel.--([:updated_at, :inserted_at, :id])
|> Enum.reject(&(schema.__schema__(:type, &1) === :id))
|> Enum.random

field_type = schema.__schema__(:type, field)

field_value = case field_type do
:integer -> "asdfd"
:string -> 1239
_ -> 4321
end

Map.put(params, field, field_value)
def build_params(factory_module, params, options) when is_list(params) do
build_params(factory_module, Map.new(params), options)
end

@doc """
Builds a schema given the factory `module` and an optional
list/map of `params`.
Builds many parameters and associations for a schema `changeset/2` function
given the factory `module` and an optional list/map of `params`.
"""
@spec build(module()) :: Ecto.Schema.t()
@spec build(module(), keyword() | map()) :: Ecto.Schema.t()
def build(module, params \\ %{}, options \\ [])

def build(module, params, options) when is_list(params) do
build(module, Map.new(params), options)
end

def build(module, params, options) do
validate = Keyword.get(options, :validate, true)

params
|> module.build()
|> maybe_changeset(module, validate)
|> case do
%Ecto.Changeset{} = changeset -> Ecto.Changeset.apply_action!(changeset, :insert)
struct when is_struct(struct) -> struct
end
@spec build_many_params(pos_integer(), module()) :: [map(), ...]
@spec build_many_params(pos_integer(), module(), keyword() | map()) :: [map(), ...]
@spec build_many_params(pos_integer(), module(), keyword() | map(), build_options()) :: [map(), ...]
def build_many_params(count, factory_module, params \\ %{}, options \\ [])
when is_integer(count) and count > 0 do
Enum.map(1..count, fn _ -> build_params(factory_module, params, options) end)
end

@doc """
Inserts a schema given the factory `module` and an optional list/map of
`params`. Fails on error.
Builds the params for a schema's changeset function is a random field
given an incorrect value for the type.
"""
@spec insert!(module()) :: Ecto.Schema.t() | no_return()
@spec insert!(module(), keyword() | map(), Keyword.t()) :: Ecto.Schema.t() | no_return()
def insert!(module, params \\ %{}, options \\ [])

def insert!(module, params, options) when is_list(params) do
insert!(module, Map.new(params), options)
end

def insert!(module, params, options) do
validate = Keyword.get(options, :validate, true)

params
|> module.build()
|> maybe_changeset(module, validate)
|> module.repo().insert!(options)
end
@spec build_invalid_params(module()) :: map()
defdelegate build_invalid_params(factory_module), to: FactoryEx.SchemaBuilder, as: :build_invalid_params

@doc """
Insert as many as `count` schemas given the factory `module` and an optional
Builds a schema given the factory `module` and an optional
list/map of `params`.
"""
@spec insert_many!(pos_integer(), module()) :: [Ecto.Schema.t()]
@spec insert_many!(pos_integer(), module(), keyword() | map()) :: [Ecto.Schema.t()]
def insert_many!(count, module, params \\ %{}, options \\ []) when count > 0 do
Enum.map(1..count, fn _ -> insert!(module, params, options) end)
@spec build(module()) :: struct() | Ecto.Schema.t()
@spec build(module(), keyword() | map()) :: struct() | Ecto.Schema.t()
@spec build(module(), keyword() | map(), build_options()) :: struct() | Ecto.Schema.t()
def build(factory_module, params \\ %{}, options \\ [])
def build(factory_module, params, options) when is_map(params) do
options = FactoryEx.OptionsValidator.validate!(options)

factory_module.schema()
|> FactoryEx.SchemaAssociations.build_params(params, options)
|> then(&FactoryEx.SchemaBuilder.build_params(factory_module, &1, options))
end

@doc """
Removes all the instances of a schema from the database given its factory
`module`.
"""
@spec cleanup(module) :: {integer(), nil | [term()]}
def cleanup(module, options \\ []) do
module.repo().delete_all(module.schema(), options)
def build(factory_module, params, options) when is_list(params) do
build(factory_module, Map.new(params), options)
end

defp maybe_changeset(params, module, validate) do
if validate && schema?(module) do
params = Utils.deep_struct_to_map(params)

if create_changeset_defined?(module.schema()) do
module.schema().create_changeset(params)
else
module.schema().changeset(struct(module.schema(), %{}), params)
end
else
struct!(module.schema, params)
end
end

defp create_changeset_defined?(module) do
function_exported?(module, :create_changeset, 1)
end

defp schema?(module) do
function_exported?(module.schema(), :__schema__, 1)
end
end
35 changes: 35 additions & 0 deletions lib/factory_ex/app_schema_factory.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule FactoryEx.AppSchemaFactory do
defstruct [:app, :prefix, :factories]

def create(app, prefix) do
app
|> FactoryEx.Utils.Application.find_modules(prefix)
|> Enum.reduce(%{}, &maybe_put_schema_factory/2)
|> then(&struct(__MODULE__, %{app: app, prefix: prefix, factories: &1}))
end

defp maybe_put_schema_factory(module, acc) when is_atom(module) do
if FactoryEx.Utils.Code.ensure_function_exported?(module, :schema, 0) do
Map.put(acc, module.schema(), module)
else
acc
end
end

def build_params(%__MODULE__{} = mapper, ecto_schema, params \\ %{}) do
case Map.get(mapper.factories, ecto_schema) do
nil ->
raise FactoryEx.Exceptions.RuntimeError,
message: """
No Factory module found for `#{ecto_schema}`! Unable to build params.
Expected the `#{inspect(mapper.app)}` app to define the module with the prefix `#{inspect(mapper.prefix)}`.
"""

factory -> factory.build(params)
end
end

def build_many_params(count, %__MODULE__{} = mapper, ecto_schema, params \\ %{}) do
Enum.map(1..count, fn _ -> build_params(mapper, ecto_schema, params) end)
end
end
3 changes: 3 additions & 0 deletions lib/factory_ex/exceptions/argument_error.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule FactoryEx.Exceptions.ArgumentError do
defexception [:message]
end
3 changes: 3 additions & 0 deletions lib/factory_ex/exceptions/runtime_error.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule FactoryEx.Exceptions.RuntimeError do
defexception [:message]
end
65 changes: 65 additions & 0 deletions lib/factory_ex/options_validator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
defmodule FactoryEx.OptionsValidator do
@moduledoc """
FactoryEx Options Validator
This module defines the NimbleOptions schema for validating options.
"""

@build_schema_definition [
keys: [
type: {:in, [:atom, :string, :camel_string]},
doc: "Sets the type of keys to have in the built object, can be one of `:atom`, `:string` or `:camel_string`"
],
except: [
type: {:list, :any},
doc: "Sets the keys to drop from an enumerable. Can be a list of `:atom` or `:map`"
],
only: [
type: {:list, :any},
doc: "Sets the keys to take from an enumerable. Can be a list of `:atom` or `:map`"
],
validate: [
type: :boolean,
doc: "Validate the param keys with `struct/2` or `changeset/2`. Can be `true` or `false`"
]
]

@build_assoc_definition [
relational: [
type: :any,
doc: "Sets the association fields that are autogenerated by the association loader, can be `:all` or a list of `:atom` of the key."
]
]

@build_schema_factory_definition [
app: [
type: :atom,
doc: "Sets the app to load the factory modules. Can be a `:atom`."
],
app: [
type: :atom,
doc: "Sets the prefix of the factory modules to load. Can be a module `:atom`."
]
]

@build_definition @build_schema_definition
++ @build_assoc_definition
++ @build_schema_factory_definition

@doc """
FactoryEx Build Options Schema
"""
@spec schema :: Keyword.t()
def schema, do: @build_definition

@doc """
Validate Build Options.
Raises on error.
"""
@spec validate! :: list()
def validate!(options \\ []) do
NimbleOptions.validate!(options, @build_definition)
end

end
Loading

0 comments on commit 1c3e75c

Please sign in to comment.