From 2f8cd3538ecb9d1a71f26154952759a491e8f4ad Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Fri, 31 Mar 2023 08:40:31 -0700 Subject: [PATCH 01/26] feat: ecto association builder --- .tool-versions | 2 + config/config.exs | 10 +- lib/factory_ex.ex | 94 +++++-- lib/factory_ex/application.ex | 35 +++ lib/factory_ex/association_builder.ex | 175 +++++++++++++ lib/factory_ex/factory_store.ex | 138 ++++++++++ lib/factory_ex/utils.ex | 94 ++++++- lib/mix/factory_ex_helpers.ex | 80 ++++-- lib/mix/tasks/gen.ex | 242 +++++++++++------- mix.exs | 16 +- ...230331151546_create_team_organizations.exs | 9 + .../20230331151636_create_teams.exs | 11 + .../20230331151723_create_account_roles.exs | 9 + .../20230331151733_create_account_labels.exs | 9 + .../20230331152052_create_account_users.exs | 20 ++ ...30331152519_create_account_user_labels.exs | 10 + test/factory_ex_test.exs | 92 ++++++- test/support/accounts/role.ex | 25 -- test/support/accounts/team.ex | 26 -- test/support/accounts/team_organization.ex | 23 -- test/support/accounts/user.ex | 47 ---- test/support/datacase.ex | 62 +++++ test/support/factory/accounts/label.ex | 17 ++ test/support/factory/accounts/role.ex | 17 ++ test/support/factory/accounts/team.ex | 17 ++ .../factory/accounts/team_organization.ex | 17 ++ test/support/factory/accounts/user.ex | 23 ++ test/support/{ => schema}/accounts/label.ex | 13 +- test/support/schema/accounts/role.ex | 23 ++ test/support/schema/accounts/team.ex | 24 ++ .../schema/accounts/team_organization.ex | 23 ++ test/support/schema/accounts/user.ex | 48 ++++ test/test_helper.exs | 2 + 33 files changed, 1154 insertions(+), 299 deletions(-) create mode 100644 .tool-versions create mode 100644 lib/factory_ex/application.ex create mode 100644 lib/factory_ex/association_builder.ex create mode 100644 lib/factory_ex/factory_store.ex create mode 100644 priv/repo/migrations/20230331151546_create_team_organizations.exs create mode 100644 priv/repo/migrations/20230331151636_create_teams.exs create mode 100644 priv/repo/migrations/20230331151723_create_account_roles.exs create mode 100644 priv/repo/migrations/20230331151733_create_account_labels.exs create mode 100644 priv/repo/migrations/20230331152052_create_account_users.exs create mode 100644 priv/repo/migrations/20230331152519_create_account_user_labels.exs delete mode 100644 test/support/accounts/role.ex delete mode 100644 test/support/accounts/team.ex delete mode 100644 test/support/accounts/team_organization.ex delete mode 100644 test/support/accounts/user.ex create mode 100644 test/support/datacase.ex create mode 100644 test/support/factory/accounts/label.ex create mode 100644 test/support/factory/accounts/role.ex create mode 100644 test/support/factory/accounts/team.ex create mode 100644 test/support/factory/accounts/team_organization.ex create mode 100644 test/support/factory/accounts/user.ex rename test/support/{ => schema}/accounts/label.ex (53%) create mode 100644 test/support/schema/accounts/role.ex create mode 100644 test/support/schema/accounts/team.ex create mode 100644 test/support/schema/accounts/team_organization.ex create mode 100644 test/support/schema/accounts/user.ex diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..ee4c41c --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +erlang 25.1 +elixir 1.13.4-otp-25 diff --git a/config/config.exs b/config/config.exs index 955391a..13f7881 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,9 +1,11 @@ import Config -config :factory_ex, FactoryExTest.MyRepo, - username: System.get_env("POSTGRES_USER") || "postgres", - password: System.get_env("POSTGRES_PASSWORD") || "postgres", +config :factory_ex, ecto_repos: [FactoryEx.Support.Repo] +config :factory_ex, :sql_sandbox, true +config :factory_ex, FactoryEx.Support.Repo, + username: "postgres", + password: "postgres", database: "factory_ex_test", hostname: "localhost", pool: Ecto.Adapters.SQL.Sandbox, - pool_size: String.to_integer(System.get_env("POSTGRES_POOL_SIZE", "10")) + pool_size: 5 diff --git a/lib/factory_ex.ex b/lib/factory_ex.ex index 590ff16..1f4cd9e 100644 --- a/lib/factory_ex.ex +++ b/lib/factory_ex.ex @@ -2,7 +2,16 @@ 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`" + doc: + "Sets the type of keys to have in the built object, can be one of `:atom`, `:string` or `:camel_string`" + ], + relational: [ + type: {:or, [{:list, :atom}, :keyword_list]}, + doc: "Sets the ecto schema association fields to generate, can be a list of `:atom` or `:keyword_list`" + ], + check_owner_key?: [ + type: :boolean, + doc: "Sets the behaviour for handling associated parameters when the owner key is set, can be `true` or `false`. Defaults to `true`." ] ] @@ -15,11 +24,11 @@ defmodule FactoryEx do #{NimbleOptions.docs(@build_definition)} """ - alias FactoryEx.Utils + alias FactoryEx.{AssociationBuilder, Utils} @type build_opts :: [ - keys: :atom | :string | :camel_string - ] + keys: :atom | :string | :camel_string + ] @doc """ Callback that returns the schema module. @@ -72,7 +81,9 @@ defmodule FactoryEx do opts = NimbleOptions.validate!(opts, @build_definition) params + |> Utils.expand_count_tuples() |> module.build() + |> then(&AssociationBuilder.build_params(module, &1, opts)) |> Utils.deep_struct_to_map() |> maybe_encode_keys(opts) end @@ -94,18 +105,20 @@ defmodule FactoryEx do schema = module.schema() Code.ensure_loaded(schema) - field = schema.__schema__(:fields) + field = + schema.__schema__(:fields) |> Kernel.--([:updated_at, :inserted_at, :id]) |> Enum.reject(&(schema.__schema__(:type, &1) === :id)) - |> Enum.random + |> Enum.random() field_type = schema.__schema__(:type, field) - field_value = case field_type do - :integer -> "asdfd" - :string -> 1239 - _ -> 4321 - end + field_value = + case field_type do + :integer -> "asdfd" + :string -> 1239 + _ -> 4321 + end Map.put(params, field, field_value) end @@ -124,11 +137,13 @@ defmodule FactoryEx do def build(module, params, options) do Code.ensure_loaded(module.schema()) - validate = Keyword.get(options, :validate, true) + validate? = Keyword.get(options, :validate, true) params + |> Utils.expand_count_tuples() |> module.build() - |> maybe_changeset(module, validate) + |> then(&AssociationBuilder.build_params(module, &1, options)) + |> maybe_create_changeset(module, validate?) |> case do %Ecto.Changeset{} = changeset -> Ecto.Changeset.apply_action!(changeset, :insert) struct when is_struct(struct) -> struct @@ -152,8 +167,10 @@ defmodule FactoryEx do validate? = Keyword.get(options, :validate, true) params + |> Utils.expand_count_tuples() |> module.build() - |> maybe_changeset(module, validate?) + |> then(&AssociationBuilder.build_params(module, &1, options)) + |> maybe_create_changeset(module, validate?) |> module.repo().insert!(options) end @@ -176,20 +193,61 @@ defmodule FactoryEx do module.repo().delete_all(module.schema(), options) end - defp maybe_changeset(params, module, validate?) do + defp maybe_create_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) + params + |> module.schema().create_changeset() + |> maybe_put_assocs(params) else - module.schema().changeset(struct(module.schema(), %{}), params) + module.schema() + |> struct(%{}) + |> module.schema().changeset(params) + |> maybe_put_assocs(params) end else - struct!(module.schema, params) + deep_struct!(module.schema, params) end end + defp maybe_put_assocs(%{data: %module{}} = changeset, params) do + :associations + |> module.__schema__() + |> Enum.reduce(changeset, fn field, changeset -> + case Map.get(params, field) do + nil -> changeset + attrs -> Ecto.Changeset.put_assoc(changeset, field, attrs) + end + end) + end + + defp deep_struct!(schema_module, params) when is_list(params) do + Enum.map(params, &deep_struct!(schema_module, &1)) + end + + defp deep_struct!(schema_module, params) do + Enum.reduce(params, struct!(schema_module, params), &convert_to_struct(&1, schema_module, &2)) + end + + defp convert_to_struct({field, attrs}, schema_module, acc) do + attrs = + case :associations + |> schema_module.__schema__() + |> Enum.find(&(&1 === field)) + |> then(&schema_module.__schema__(:association, &1)) do + nil -> + attrs + + ecto_assoc -> + deep_struct!(ecto_assoc.queryable, attrs) + + end + + Map.put(acc, field, attrs) + end + defp create_changeset_defined?(module) do function_exported?(module, :create_changeset, 1) end diff --git a/lib/factory_ex/application.ex b/lib/factory_ex/application.ex new file mode 100644 index 0000000..f173c91 --- /dev/null +++ b/lib/factory_ex/application.ex @@ -0,0 +1,35 @@ +defmodule FactoryEx.Application do + @moduledoc false + + use Application + + @impl true + def start(_, _) do + FactoryEx.SchemaCounter.start() + + children = [ + FactoryEx.FactoryStore + ] + + opts = [strategy: :one_for_one, name: FactoryEx.Supervisor] + Supervisor.start_link(children, opts) + end + + def apps_that_depend_on(dep) do + :application.loaded_applications() + |> Enum.reduce([], fn {app, _, _}, acc -> + deps = Application.spec(app)[:applications] + (dep in deps && acc ++ [app]) || acc + end) + end + + def find_app_modules(app, prefix) do + case :application.get_key(app, :modules) do + {:ok, modules} -> + prefix = Module.split(prefix) + Enum.filter(modules, &(&1 |> Module.split() |> FactoryEx.Utils.sublist?(prefix))) + + _ -> raise "modules not found for app #{inspect(app)}." + end + end +end diff --git a/lib/factory_ex/association_builder.ex b/lib/factory_ex/association_builder.ex new file mode 100644 index 0000000..52d7f09 --- /dev/null +++ b/lib/factory_ex/association_builder.ex @@ -0,0 +1,175 @@ +defmodule FactoryEx.AssociationBuilder do + @moduledoc """ + + This module implements the api for auto generating Ecto Associations with factories. + + To use this api you must pass keys (which can be type of `:list` or `:keyword_list`) to the + `relational` option. These keys are used by the `build_params/3` to find the appropriate + Factory for an Ecto Schema and invoke the `build_params/3` callback function. This allows you + to build any relational data structure declaratively. + + In the following section we will explain how `build_params/3` works with relational keys: + + - For each relational key, we try to fetch the the ecto schema's associations. If the field does + not exist for the given schema an error is raised otherwise the association is used to create + one or many params for the field based on the association's cardinality. + + - If the `owner_key` of the association's schema field is not set, the factory's `build/1` + function will be invoked with the field's existing value. If the owner key is set the + field is skipped and the existing value will be kept. Any existing paramaters not passed + as a relational key will be kept. If this behaviour is not desired you can set the + `check_owner_field?` option to `false` and the parameters will be generated when the + owner key is set. + + ## Examples + + # Create params for a one-to-one relationship + FactoryEx.AssociationBuilder.build_params( + FactoryEx.Support.Factory.Accounts.User, + %{pre_existing_params: "hello world!"}, + relational: [:role, :team] + ) + %{ + pre_existing_params: "hello world!", + role: %{code: "Utah cats"}, + team: %{name: "Macejkovic Group"} + } + + # Create params for a one-to-many relationship + FactoryEx.AssociationBuilder.build_params( + FactoryEx.Support.Factory.Accounts.TeamOrganization, + %{teams: [%{}, %{}]}, + relational: [:teams] + ) + %{teams: [%{name: "Lindgren-Zemlak"}, %{name: "Kutch Group"}]} + + # Create deep relational structure and override specific field values + FactoryEx.AssociationBuilder.build_params( + FactoryEx.Support.Factory.Accounts.TeamOrganization, + %{teams: [%{name: "team name goes here", users: [%{name: "first user name"}, %{}]}]}, + relational: [teams: [users: [:labels, :role]]] + ) + %{ + teams: [ + %{ + name: "team name goes here", + users: [ + %{ + birthday: ~D[1992-10-04], + email: "ivy_lind@braun.info", + gender: "male", + labels: [%{label: "expedita"}], + location: "someplace", + name: "first user name", + role: %{code: "Iowa penguins"} + }, + %{ + birthday: ~D[1992-10-04], + email: "dillan1930@nolan.biz", + gender: "male", + labels: [%{label: "exercitationem"}], + location: "someplace", + name: "Name Zulauf Jr.", + role: %{code: "New Hampshire dwarves"} + } + ] + } + ] + } + """ + + @doc """ + Builds Ecto Association parameters. + """ + @spec build_params(module(), map(), Keyword.t()) :: map() + def build_params(factory_module, params \\ %{}, options \\ []) do + schema = factory_module.schema() + assoc_fields = Keyword.get(options, :relational, []) + check_owner_key? = Keyword.get(options, :check_owner_key?, true) + + convert_fields_to_params(schema, params, assoc_fields, check_owner_key?) + end + + defp convert_fields_to_params(schema, params, assoc_fields, check_owner_key?) do + Enum.reduce(assoc_fields, params, &create_schema_params(schema, &1, &2, check_owner_key?)) + end + + defp create_schema_params(schema, {field, assoc_fields}, params, check_owner_key?) do + schema + |> fetch_assoc!(field) + |> create_one_or_many_params(params, field, assoc_fields, check_owner_key?) + |> case do + nil -> params + assoc_params -> Map.put(params, field, assoc_params) + end + end + + defp create_schema_params(schema, field, params, check_owner_key?) do + create_schema_params(schema, {field, []}, params, check_owner_key?) + end + + defp create_one_or_many_params( + %{cardinality: :many, queryable: queryable} = assoc, + params, + field, + assoc_fields, + check_owner_key? + ) do + if check_owner_key? and owner_key_is_set?(assoc, params) do + Map.get(params, field) + else + params = Map.get(params, field, [%{}]) + Enum.map(params, fn params -> factory_build(queryable, params, assoc_fields, check_owner_key?) end) + end + end + + defp create_one_or_many_params( + %{cardinality: :one, queryable: queryable} = assoc, + params, + field, + assoc_fields, + check_owner_key? + ) do + if check_owner_key? and owner_key_is_set?(assoc, params) do + Map.get(params, field) + else + params = Map.get(params, field, %{}) + factory_build(queryable, params, assoc_fields, check_owner_key?) + end + end + + defp owner_key_is_set?(assoc, params) do + case Map.get(params, assoc.owner_key) do + nil -> false + _ -> true + end + end + + defp factory_build(queryable, params, assoc_fields, check_owner_key?) do + parent = FactoryEx.FactoryStore.build_params(queryable, params) + assoc = convert_fields_to_params(queryable, params, assoc_fields, check_owner_key?) + + Map.merge(parent, assoc) + end + + defp fetch_assoc!(schema, field) do + assocs = schema.__schema__(:associations) + + if Enum.member?(assocs, field) do + schema.__schema__(:association, field) + else + raise """ + The field '#{inspect(field)}' you entered was not found on schema '#{inspect(schema)}'. + + Did you mean one of the following fields? + #{inspect(assocs)} + + To fix this error: + + - Ensure the field exists on the schema '#{inspect(schema)}'. + + - Return a schema from the `schema/0` callback function that contains the field '#{inspect(field)}'. + """ + end + end +end diff --git a/lib/factory_ex/factory_store.ex b/lib/factory_ex/factory_store.ex new file mode 100644 index 0000000..f0fec6c --- /dev/null +++ b/lib/factory_ex/factory_store.ex @@ -0,0 +1,138 @@ +defmodule FactoryEx.FactoryStore do + @moduledoc """ + A simple key-value store that maps Ecto.Schema modules to Factory modules. + + ## Requirements + + - Your application defines a factory module for each schema used as a relational key + - Your application has `factory_ex` as a depedency in `mix.exs`. + - Your module contains the prefix `Factory`, ie. YourApp.Support.Factory.Schemas.Schema. + - Your factory module defines the factory_ex `schema/0` callback function. + + Factory modules are stored in an ets table with their schema values as the key. + This allows us to invoke the the appropriate factory module for a given schema. + + Let's look at an example: + + Call `FactoryEx.FactoryStore.all/0` to see all discovered factory modules. + By default the modules defined by `factory_ex` will be displayed. + + ```elixir + FactoryEx.FactoryStore.all + [ + {FactoryEx.Support.Schema.Accounts.User, FactoryEx.Support.Factory.Accounts.User}, + {FactoryEx.Support.Schema.Accounts.TeamOrganization, FactoryEx.Support.Factory.Accounts.TeamOrganization}, + {FactoryEx.Support.Schema.Accounts.Label, FactoryEx.Support.Factory.Accounts.Label}, + {FactoryEx.Support.Schema.Accounts.Role, FactoryEx.Support.Factory.Accounts.Role}, + {FactoryEx.Support.Schema.Accounts.Team, FactoryEx.Support.Factory.Accounts.Team} + ] + ``` + + To build the params for the schema `FactoryEx.Support.Schema.Accounts.User` we call + `FactoryEx.FactoryStore.build_params/2` with the schema module. Arguments are + passed to the factory module's `build/1` callback functions. + + ## Usage Examples + + FactoryEx.FactoryStore.build_params(FactoryEx.Support.Schema.Accounts.User) + %{ + birthday: ~D[1992-10-04], + email: "tyrese_welch@beier.org", + gender: "male", + location: "someplace", + name: "Elisa Abbott" + } + + ## Factory Module Prefix + + If your application's factory modules do not use the prefix `Factory` or you want to change + which factory modules are loaded during tests you can configure the module prefix option at + compile time with the following config: + + ```elixir + config :factory_ex, :factory_module_prefix, Factory + ``` + """ + use Task + + @tab :factory_ex_factory_store + @tab_options [ + :set, + :named_table, + :public, + read_concurrency: true, + write_concurrency: true + ] + + @app :factory_ex + @factory_prefix Application.compile_env(@app, :factory_module_prefix, Factory) + + @doc false + @spec start_link(any) :: {:ok, pid} + def start_link(_) do + Task.start_link(fn -> + :ets.new(@tab, @tab_options) + init() + + Process.hibernate(Function, :identity, []) + end) + end + + defp init do + [@app | FactoryEx.Application.apps_that_depend_on(@app)] + |> Enum.reduce(%{}, &lookup_factory_modules/2) + |> Enum.map(&:ets.insert(@tab, &1)) + end + + defp lookup_factory_modules(app, acc) do + app + |> FactoryEx.Application.find_app_modules(@factory_prefix) + |> Enum.reduce(acc, &maybe_put_factory_module/2) + end + + defp maybe_put_factory_module(module, acc) do + if FactoryEx.Utils.ensure_function_exported?(module, :schema, 0) do + Map.put(acc, module.schema(), module) + else + acc + end + end + + defp fetch_schema_factory!(ecto_schema) do + case @tab |> :ets.lookup(ecto_schema) |> Keyword.get(ecto_schema) do + nil -> + raise """ + Factory not found for schema '#{inspect(ecto_schema)}'. + + This means the factory module does not exist or it was not loaded. + + To fix this error: + + - Add `factory_ex` as a depedency in `mix.exs` of the application that contains + the schema '#{inspect(ecto_schema)}'. In umbrella applications you must add + `factory_ex` as a dependency to each application that contain factory modules. + + - Create a factory for the schema '#{inspect(ecto_schema)}' + + - Add the prefix '#{inspect(@factory_prefix)}' to the factory module name. For example: + YourApp.#{inspect(@factory_prefix)}.Module. + """ + + factory -> factory + end + end + + @doc """ + Returns all factories in the ets table + """ + @spec all() :: list({module, module}) | [] + def all, do: :ets.tab2list(@tab) + + @doc """ + Returns the result of the `build/1` function of the Factory associated with the given schema. + """ + @spec build_params(module, map) :: map + def build_params(ecto_schema, params \\ %{}) do + fetch_schema_factory!(ecto_schema).build(params) + end +end diff --git a/lib/factory_ex/utils.ex b/lib/factory_ex/utils.ex index 10daec2..4193bb3 100644 --- a/lib/factory_ex/utils.ex +++ b/lib/factory_ex/utils.ex @@ -38,17 +38,17 @@ defmodule FactoryEx.Utils do end def underscore_schema(ecto_schema) do - ecto_schema |> String.replace(".", "") |> Macro.underscore + ecto_schema |> String.replace(".", "") |> Macro.underscore() end def context_schema_name(ecto_schema) do ecto_schema - |> String.split(".") - |> Enum.take(-2) - |> Enum.map_join("_", &Macro.underscore/1) + |> String.split(".") + |> Enum.take(-2) + |> Enum.map_join("_", &Macro.underscore/1) end - @doc """ + @doc """ Converts all string keys to string ### Example @@ -106,4 +106,88 @@ defmodule FactoryEx.Utils do defp camelize_list([h | tail], :upper) do [String.capitalize(h)] ++ camelize_list(tail, :upper) end + + @doc """ + Returns `true` if the second list exists in the first list or `false`. + + ## Example + iex> FactoryEx.Util.List.sublist?([:a, :b, :c], [:b, :c]) + true + """ + def sublist?([], _), do: false + + def sublist?([_ | t] = l1, l2) do + List.starts_with?(l1, l2) or sublist?(t, l2) + end + + @doc """ + Ensure the module with the public function and arity is defined + + Note: `function_exported/3` does not load the module in case it is not loaded. + If the BEAM is running in `interactive` mode there is a chance this module has not + been loaded yet. `Code.ensure_loaded/1` is used to ensure the module is loaded + first. + + Docs: https://hexdocs.pm/elixir/1.12/Kernel.html#function_exported?/3 + """ + def ensure_function_exported?(module, fun, arity) do + case Code.ensure_loaded(module) do + {:module, module} -> + function_exported?(module, fun, arity) + + {:error, reason} -> + raise """ + Code failed to load module `#{inspect(module)}` with reason: #{inspect(reason)}! + Ensure the module name is correct and it exists. + """ + end + end + + @doc """ + Deep Converts `{count, attrs}` to list of `attrs`. + + ## Examples + + iex> FactoryEx.TermExpander.expand_count_tuples(%{hello: {2, %{world: {2, %{foo: :bar}}}}}) + %{ + hello: [ + %{world: [%{foo: :bar}, %{foo: :bar}]}, + %{world: [%{foo: :bar}, %{foo: :bar}]} + ] + } + + iex> FactoryEx.TermExpander.expand_count_tuples(%{hello: [%{foo: "bar"}, {2, %{}}]}) + %{hello: [%{foo: "bar"}, %{}, %{}]} + + iex> FactoryEx.TermExpander.expand_count_tuples(%{hello: [%{foo: {1, %{}}}, {1, %{qux: {1, %{bux: "hello world"}}}}]}) + %{hello: [%{foo: [%{}]}, %{qux: [%{bux: "hello world"}]}]} + """ + @spec expand_count_tuples(map() | list()) :: map() + def expand_count_tuples(enum), do: enum |> Enum.map(&transform/1) |> Map.new() + + defp expand_many_count_tuples(count, attrs), do: Enum.map(1..count, fn _ -> expand_count_tuples(attrs) end) + + defp transform({key, attrs}) when is_map(attrs), do: {key, expand_count_tuples(attrs)} + + defp transform({key, many_attrs}) when is_list(many_attrs) do + attrs = + Enum.reduce(many_attrs, [], fn + {count, attrs}, acc -> + acc ++ expand_many_count_tuples(count, attrs) + + attrs, acc -> + acc ++ [expand_count_tuples(attrs)] + + end) + + {key, attrs} + end + + defp transform({key, {count, attrs}}) do + {key, expand_many_count_tuples(count, attrs)} + end + + defp transform(attrs) do + attrs + end end diff --git a/lib/mix/factory_ex_helpers.ex b/lib/mix/factory_ex_helpers.ex index 82a67e5..e161b3d 100644 --- a/lib/mix/factory_ex_helpers.ex +++ b/lib/mix/factory_ex_helpers.ex @@ -5,22 +5,31 @@ defmodule Mix.FactoryExHelpers do def ensure_not_in_umbrella!(command) do if Mix.Project.umbrella?() do - Mix.raise("mix #{command} must be invoked from within your *_web application root directory") + Mix.raise( + "mix #{command} must be invoked from within your *_web application root directory" + ) end end def string_to_module(module_string) do Module.safe_concat([module_string]) - - rescue - ArgumentError -> - reraise to_string(IO.ANSI.format([ - :red, "Module ", :bright, module_string, - :reset, :red, " doesn't exist", :reset - ])), __STACKTRACE__ + rescue + ArgumentError -> + reraise to_string( + IO.ANSI.format([ + :red, + "Module ", + :bright, + module_string, + :reset, + :red, + " doesn't exist", + :reset + ]) + ), + __STACKTRACE__ end - def schema_primary_key(ecto_schema) do ecto_schema.__schema__(:primary_key) end @@ -49,41 +58,62 @@ defmodule Mix.FactoryExHelpers do def schema_association_module(ecto_schema, relation_name) do case schema_association(ecto_schema, relation_name) do - %{queryable: queryable} -> queryable + %{queryable: queryable} -> + queryable + _ -> - raise to_string(IO.ANSI.format([ - :red, :bright, to_string(relation_name), :reset, - :red, "doesn't exist on ", :bright, inspect(ecto_schema), :reset, - :red, ", check your config for this key" - ], true)) + raise to_string( + IO.ANSI.format( + [ + :red, + :bright, + to_string(relation_name), + :reset, + :red, + "doesn't exist on ", + :bright, + inspect(ecto_schema), + :reset, + :red, + ", check your config for this key" + ], + true + ) + ) end end def schema_module(ecto_schema) do ecto_schema - |> Module.split - |> List.last + |> Module.split() + |> List.last() end def schema_module_resource_name(ecto_schema) do ecto_schema - |> schema_module - |> Macro.underscore + |> schema_module + |> Macro.underscore() end def fields_intersection(fields_a, fields_b) do fields_a - |> MapSet.new - |> MapSet.intersection(MapSet.new(fields_b)) - |> MapSet.to_list + |> MapSet.new() + |> MapSet.intersection(MapSet.new(fields_b)) + |> MapSet.to_list() end def schema_relationship_list?(ecto_schema, relation_name) do case schema_association(ecto_schema, relation_name) do - %{cardinality: :one} -> false - %{cardinality: :many} -> true + %{cardinality: :one} -> + false + + %{cardinality: :many} -> + true + assication_record -> - Logger.error("[PhoenixConfig.EctoUtils] Error checking if schema #{ecto_schema} relationship #{relation_name} is a list\n#{inspect assication_record}") + Logger.error( + "[PhoenixConfig.EctoUtils] Error checking if schema #{ecto_schema} relationship #{relation_name} is a list\n#{inspect(assication_record)}" + ) false end diff --git a/lib/mix/tasks/gen.ex b/lib/mix/tasks/gen.ex index c63ab72..59dd8b1 100644 --- a/lib/mix/tasks/gen.ex +++ b/lib/mix/tasks/gen.ex @@ -25,50 +25,66 @@ defmodule Mix.Tasks.FactoryEx.Gen do ] @faker_functions (case :application.get_key(:faker, :modules) do - {:ok, modules} -> - modules - |> Enum.filter(fn module -> - module_level = module - |> inspect - |> String.codepoints - |> Enum.filter(&(&1 === ".")) - |> length - - module_level === 1 - end) - |> Enum.map(fn module -> - functions = module.__info__(:functions) - |> Enum.filter(fn {_name, arity} -> arity === 0 end) - |> Enum.map(fn {name, _arity} -> name end) - - {module, functions} - end) - |> Enum.filter(fn {_module, functions} -> length(functions) > 0 end) - - e -> - throw "Some weird error happened when trying to find faker functions\n#{inspect e, pretty: true}" - end) + {:ok, modules} -> + modules + |> Enum.filter(fn module -> + module_level = + module + |> inspect + |> String.codepoints() + |> Enum.filter(&(&1 === ".")) + |> length + + module_level === 1 + end) + |> Enum.map(fn module -> + functions = + module.__info__(:functions) + |> Enum.filter(fn {_name, arity} -> arity === 0 end) + |> Enum.map(fn {name, _arity} -> name end) + + {module, functions} + end) + |> Enum.filter(fn {_module, functions} -> length(functions) > 0 end) + + e -> + throw( + "Some weird error happened when trying to find faker functions\n#{inspect(e, pretty: true)}" + ) + end) def run(args) do Mix.Task.run("app.config", []) FactoryExHelpers.ensure_not_in_umbrella!("factory_ex.gen.factory") - {opts, extra_args, _} = OptionParser.parse(args, - switches: [ - dirname: :string, - app_name: :string, - force: :boolean, - quiet: :boolean, - repo: :string - ] - ) + {opts, extra_args, _} = + OptionParser.parse(args, + switches: [ + dirname: :string, + app_name: :string, + force: :boolean, + quiet: :boolean, + repo: :string + ] + ) if opts[:app_name] && opts[:dirname] do - raise to_string(IO.ANSI.format([ - :red, "Only one of ", :bright, "app_name", :reset, - :red, " or ", :bright, "dirname", :reset, - :red, " should be supplied" - ])) + raise to_string( + IO.ANSI.format([ + :red, + "Only one of ", + :bright, + "app_name", + :reset, + :red, + " or ", + :bright, + "dirname", + :reset, + :red, + " should be supplied" + ]) + ) end if validate_repo?(opts[:repo]) do @@ -86,40 +102,59 @@ defmodule Mix.Tasks.FactoryEx.Gen do true else - Mix.shell().error(to_string(IO.ANSI.format([ - :reset, :red, "Must provide ", :bright, "--repo", :reset, - :red, " when using factory_ex.gen", :reset - ]))) + Mix.shell().error( + to_string( + IO.ANSI.format([ + :reset, + :red, + "Must provide ", + :bright, + "--repo", + :reset, + :red, + " when using factory_ex.gen", + :reset + ]) + ) + ) false end end def ensure_schema_counter_start_added(opts) do - directory = cond do - opts[:dirname] -> opts[:dirname] - opts[:app_name] -> Path.expand(Path.join(["../", opts[:app_name]])) - true -> "." - end + directory = + cond do + opts[:dirname] -> opts[:dirname] + opts[:app_name] -> Path.expand(Path.join(["../", opts[:app_name]])) + true -> "." + end "#{directory}/**/test_helper.exs" - |> Path.wildcard - |> Enum.each(fn path -> - path = Path.expand(path) + |> Path.wildcard() + |> Enum.each(fn path -> + path = Path.expand(path) - contents = File.read!(path) + contents = File.read!(path) - if not String.contains?(contents, "FactoryEx.SchemaCounter.start()") do - path = Path.relative_to_cwd(path) - Mix.shell().info([:green, "* injecting FactoryEx.SchemaCounter.start() into ", :reset, path]) + if not String.contains?(contents, "FactoryEx.SchemaCounter.start()") do + path = Path.relative_to_cwd(path) - File.write!(path, contents <> "\nFactoryEx.SchemaCounter.start()", opts) - end - end) + Mix.shell().info([ + :green, + "* injecting FactoryEx.SchemaCounter.start() into ", + :reset, + path + ]) + + File.write!(path, contents <> "\nFactoryEx.SchemaCounter.start()", opts) + end + end) end def generate_factory(ecto_schema, repo, opts) do - schema_fields = ecto_schema + schema_fields = + ecto_schema |> FactoryExHelpers.schema_fields() |> Kernel.--(FactoryExHelpers.schema_primary_key(ecto_schema) ++ @blacklist_fields) |> FactoryExHelpers.with_field_types(ecto_schema) @@ -132,11 +167,17 @@ defmodule Mix.Tasks.FactoryEx.Gen do end defp schema_factory_path(ecto_schema, opts) do - dirname = cond do - opts[:dirname] -> opts[:dirname] - opts[:app_name] -> Path.expand(Path.join(["../", opts[:app_name], "test/support/factory"])) - true -> Path.expand("./test/support/factory/") - end + dirname = + cond do + opts[:dirname] -> + opts[:dirname] + + opts[:app_name] -> + Path.expand(Path.join(["../", opts[:app_name], "test/support/factory"])) + + true -> + Path.expand("./test/support/factory/") + end [context, schema] = ecto_schema |> inspect |> String.split(".") |> Enum.take(-2) dirname = Path.join(dirname, Macro.underscore(context)) @@ -148,8 +189,8 @@ defmodule Mix.Tasks.FactoryEx.Gen do end dirname - |> Path.join(file_name) - |> Path.relative_to_cwd + |> Path.join(file_name) + |> Path.relative_to_cwd() end defp factory_template(ecto_schema, repo, schema_fields, _opts) do @@ -175,9 +216,12 @@ defmodule Mix.Tasks.FactoryEx.Gen do defp ecto_schema_factory_module(ecto_schema) do [root_module | nested_modules] = ecto_schema |> inspect |> String.split(".") - other_modules = nested_modules + + other_modules = + nested_modules |> Enum.join(".") - |> String.replace(~r/^Support\./, "") # This is just to support tests + # This is just to support tests + |> String.replace(~r/^Support\./, "") "#{root_module}.Support.Factory.#{other_modules}" end @@ -187,7 +231,8 @@ defmodule Mix.Tasks.FactoryEx.Gen do end defp build_random_field(:integer, field, ecto_schema) do - schema_name = ecto_schema + schema_name = + ecto_schema |> inspect() |> String.split(".") |> Enum.map_join("_", &Macro.underscore/1) @@ -206,7 +251,9 @@ defmodule Mix.Tasks.FactoryEx.Gen do case find_faker_function_with_type(field, :string) do {module, function} -> "\"\#{#{inspect(module)}.#{function}()\}_\#{FactoryEx.SchemaCounter.next(\"#{field_name}\")\}\"" - nil -> "to_string(FactoryEx.SchemaCounter.next(\"#{field_name}\"))" + + nil -> + "to_string(FactoryEx.SchemaCounter.next(\"#{field_name}\"))" end end @@ -230,8 +277,12 @@ defmodule Mix.Tasks.FactoryEx.Gen do "Faker.Date.backward(Enum.random(100..400))" end - defp build_random_field({:parameterized, Ecto.Enum, %{mappings: mappings}}, _field, _ecto_schema) do - enum_list = mappings |> Keyword.keys |> Enum.map_join(", ", &(":#{&1}")) + defp build_random_field( + {:parameterized, Ecto.Enum, %{mappings: mappings}}, + _field, + _ecto_schema + ) do + enum_list = mappings |> Keyword.keys() |> Enum.map_join(", ", &":#{&1}") "Enum.random([#{enum_list}])" end @@ -240,31 +291,32 @@ defmodule Mix.Tasks.FactoryEx.Gen do defp find_faker_function_with_type(field, type) do field - |> matching_faker_functions - |> Enum.find(fn {module, function} -> - if module not in @faker_mod_blacklist do - faker_fn_return_type = module - |> apply(function, []) - |> resolve_type - - faker_fn_return_type === type - end - end) + |> matching_faker_functions + |> Enum.find(fn {module, function} -> + if module not in @faker_mod_blacklist do + faker_fn_return_type = + module + |> apply(function, []) + |> resolve_type + + faker_fn_return_type === type + end + end) end - defp resolve_type(%NaiveDateTime{}) do + defp resolve_type(%NaiveDateTime{}) do :naive_datetime_usec end - defp resolve_type(%DateTime{}) do + defp resolve_type(%DateTime{}) do :datetime_usec end - defp resolve_type(%Date{}) do + defp resolve_type(%Date{}) do :date end - defp resolve_type(%Time{}) do + defp resolve_type(%Time{}) do :time end @@ -294,16 +346,18 @@ defmodule Mix.Tasks.FactoryEx.Gen do def matching_faker_functions(field) do @faker_functions - |> Enum.flat_map(fn {module, functions} -> - Enum.map(functions, fn function_name -> - module_name = module |> inspect |> String.replace("Faker", "") - score = String.jaro_distance(to_string(function_name), to_string(field)) + - (String.jaro_distance(to_string(module_name), to_string(field)) / 2) - - {module, function_name, score} - end) + |> Enum.flat_map(fn {module, functions} -> + Enum.map(functions, fn function_name -> + module_name = module |> inspect |> String.replace("Faker", "") + + score = + String.jaro_distance(to_string(function_name), to_string(field)) + + String.jaro_distance(to_string(module_name), to_string(field)) / 2 + + {module, function_name, score} end) - |> Enum.sort_by(fn {_mod, _fn_name, score} -> score end, :desc) - |> Enum.map(fn {module, fn_name, _distance} -> {module, fn_name} end) + end) + |> Enum.sort_by(fn {_mod, _fn_name, score} -> score end, :desc) + |> Enum.map(fn {module, fn_name, _distance} -> {module, fn_name} end) end end diff --git a/mix.exs b/mix.exs index 8e50e44..3740a54 100644 --- a/mix.exs +++ b/mix.exs @@ -6,7 +6,8 @@ defmodule FactoryEx.MixProject do app: :factory_ex, version: "0.3.3", elixir: "~> 1.13", - description: "Factories for elixir to help create data models at random, this works for any type of ecto structs", + description: + "Factories for elixir to help create data models at random, this works for any type of ecto structs", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps(), @@ -33,7 +34,8 @@ defmodule FactoryEx.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ - extra_applications: [:logger] + extra_applications: [:logger, :postgrex], + mod: {FactoryEx.Application, []} ] end @@ -42,15 +44,11 @@ defmodule FactoryEx.MixProject do [ {:ecto, "~> 3.0"}, {:faker, ">= 0.0.0"}, - {:nimble_options, "~> 0.4"}, - {:ecto_sql, "~> 3.0", only: [:test, :dev], optional: true}, {:postgrex, "~> 0.16", only: [:test, :dev], optional: true}, - {:credo, "~> 1.6", only: [:test, :dev], runtime: false}, {:blitz_credo_checks, "~> 0.1", only: [:test, :dev], runtime: false}, - {:excoveralls, "~> 0.10", only: :test}, {:ex_doc, ">= 0.0.0", optional: true, only: :dev}, {:dialyxir, "~> 1.0", optional: true, only: :test, runtime: false} @@ -70,14 +68,12 @@ defmodule FactoryEx.MixProject do [ main: "FactoryEx", source_url: "https://github.com/theblitzapp/factory_ex", - groups_for_modules: [ - "General": [ + General: [ FactoryEx, FactoryEx.SchemaCounter ], - - "Adapters": [ + Adapters: [ FactoryEx.Adapter ] ] diff --git a/priv/repo/migrations/20230331151546_create_team_organizations.exs b/priv/repo/migrations/20230331151546_create_team_organizations.exs new file mode 100644 index 0000000..6b7c30b --- /dev/null +++ b/priv/repo/migrations/20230331151546_create_team_organizations.exs @@ -0,0 +1,9 @@ +defmodule FactoryEx.Support.Repo.Migrations.CreateTeamOrganizations do + use Ecto.Migration + + def change do + create table(:team_organizations) do + add :name, :string + end + end +end diff --git a/priv/repo/migrations/20230331151636_create_teams.exs b/priv/repo/migrations/20230331151636_create_teams.exs new file mode 100644 index 0000000..fbd0360 --- /dev/null +++ b/priv/repo/migrations/20230331151636_create_teams.exs @@ -0,0 +1,11 @@ +defmodule FactoryEx.Support.Repo.Migrations.CreateTeams do + use Ecto.Migration + + def change do + create table(:teams) do + add :name, :string + + add :team_organization_id, references(:team_organizations, on_delete: :delete_all) + end + end +end diff --git a/priv/repo/migrations/20230331151723_create_account_roles.exs b/priv/repo/migrations/20230331151723_create_account_roles.exs new file mode 100644 index 0000000..42b1cad --- /dev/null +++ b/priv/repo/migrations/20230331151723_create_account_roles.exs @@ -0,0 +1,9 @@ +defmodule FactoryEx.Support.Repo.Migrations.CreateAccountRoles do + use Ecto.Migration + + def change do + create table(:account_roles) do + add :code, :string + end + end +end diff --git a/priv/repo/migrations/20230331151733_create_account_labels.exs b/priv/repo/migrations/20230331151733_create_account_labels.exs new file mode 100644 index 0000000..abdb07d --- /dev/null +++ b/priv/repo/migrations/20230331151733_create_account_labels.exs @@ -0,0 +1,9 @@ +defmodule FactoryEx.Support.Repo.Migrations.CreateAccountLabels do + use Ecto.Migration + + def change do + create table(:account_labels) do + add :label, :text + end + end +end diff --git a/priv/repo/migrations/20230331152052_create_account_users.exs b/priv/repo/migrations/20230331152052_create_account_users.exs new file mode 100644 index 0000000..7d8ff03 --- /dev/null +++ b/priv/repo/migrations/20230331152052_create_account_users.exs @@ -0,0 +1,20 @@ +defmodule FactoryEx.Support.Repo.Migrations.CreateAccountUsers do + use Ecto.Migration + + def change do + create table(:account_users) do + add :name, :string + add :age, :integer + add :email, :string + add :email_updated_at, :utc_datetime_usec + add :location, :string + add :gender, :string + add :birthday, :date + + add :role_id, references(:account_roles, on_delete: :delete_all) + add :team_id, references(:teams, on_delete: :delete_all) + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20230331152519_create_account_user_labels.exs b/priv/repo/migrations/20230331152519_create_account_user_labels.exs new file mode 100644 index 0000000..7821fe5 --- /dev/null +++ b/priv/repo/migrations/20230331152519_create_account_user_labels.exs @@ -0,0 +1,10 @@ +defmodule FactoryEx.Support.Repo.Migrations.CreateAccountUserLabels do + use Ecto.Migration + + def change do + create table(:account_user_labels) do + add :user_id, references(:account_users, on_delete: :delete_all) + add :label_id, references(:account_labels, on_delete: :delete_all) + end + end +end diff --git a/test/factory_ex_test.exs b/test/factory_ex_test.exs index 2cc7ddb..a9362e5 100644 --- a/test/factory_ex_test.exs +++ b/test/factory_ex_test.exs @@ -1,5 +1,5 @@ defmodule FactoryExTest do - use ExUnit.Case + use FactoryEx.DataCase doctest FactoryEx defmodule MyRepo do @@ -14,9 +14,9 @@ defmodule FactoryExTest do import Ecto.Changeset schema "my_schmeas" do - field :foo, :integer - field :bar, :integer - field :foo_bar_baz, :integer + field(:foo, :integer) + field(:bar, :integer) + field(:foo_bar_baz, :integer) end @required_params [:foo, :bar] @@ -24,8 +24,8 @@ defmodule FactoryExTest do def changeset(%__MODULE__{} = user, attrs \\ %{}) do user - |> cast(attrs, @available_params) - |> validate_required(@required_params) + |> cast(attrs, @available_params) + |> validate_required(@required_params) end end @@ -64,17 +64,81 @@ defmodule FactoryExTest do test "can generate a factory with string keys" do assert %{ - "foo" => 21, - "bar" => 42, - "foo_bar_baz" => 11 - } = FactoryEx.build_params(TestFactory, %{}, keys: :string) + "foo" => 21, + "bar" => 42, + "foo_bar_baz" => 11 + } = FactoryEx.build_params(TestFactory, %{}, keys: :string) end test "can generate a factory with camelCase keys" do assert %{ - "foo" => 21, - "bar" => 42, - "fooBarBaz" => 11 - } = FactoryEx.build_params(TestFactory, %{}, keys: :camel_string) + "foo" => 21, + "bar" => 42, + "fooBarBaz" => 11 + } = FactoryEx.build_params(TestFactory, %{}, keys: :camel_string) + end + + test "can build ecto schema associations with changeset validation" do + assert %FactoryEx.Support.Schema.Accounts.TeamOrganization{ + teams: [ + %{ + users: [ + %FactoryEx.Support.Schema.Accounts.User{ + role: %FactoryEx.Support.Schema.Accounts.Role{}, + labels: [%FactoryEx.Support.Schema.Accounts.Label{}] + } + ] + } + ] + } = + FactoryEx.build( + FactoryEx.Support.Factory.Accounts.TeamOrganization, + %{}, + relational: [teams: [users: [:role, :labels]]] + ) + end + + test "can build ecto schema associations without changeset validation" do + assert %FactoryEx.Support.Schema.Accounts.TeamOrganization{ + teams: [ + %{ + users: [ + %FactoryEx.Support.Schema.Accounts.User{ + role: %FactoryEx.Support.Schema.Accounts.Role{}, + labels: [%FactoryEx.Support.Schema.Accounts.Label{}] + } + ] + } + ] + } = + FactoryEx.build( + FactoryEx.Support.Factory.Accounts.TeamOrganization, + %{}, + relational: [teams: [users: [:role, :labels]]], + validate: false + ) + end + + test "can insert" do + assert %FactoryEx.Support.Schema.Accounts.User{} = + FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.User) + + assert %FactoryEx.Support.Schema.Accounts.TeamOrganization{ + teams: [ + %{ + users: [ + %FactoryEx.Support.Schema.Accounts.User{ + role: %FactoryEx.Support.Schema.Accounts.Role{}, + labels: [%FactoryEx.Support.Schema.Accounts.Label{}] + } + ] + } + ] + } = + FactoryEx.insert!( + FactoryEx.Support.Factory.Accounts.TeamOrganization, + %{}, + relational: [teams: [users: [:role, :labels]]] + ) end end diff --git a/test/support/accounts/role.ex b/test/support/accounts/role.ex deleted file mode 100644 index 0b1eee3..0000000 --- a/test/support/accounts/role.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule FactoryEx.Support.Accounts.Role do - @moduledoc false - - use Ecto.Schema - - import Ecto.Changeset, only: [cast: 3, validate_required: 2] - - alias FactoryEx.Support.Accounts.{User, Role} - - schema "account_roles" do - field :code, :string - - has_many :users, User - end - - @required_params [:code] - - def changeset(%Role{} = user, attrs \\ %{}) do - user - |> cast(attrs, @required_params) - |> validate_required(@required_params) - end -end - - diff --git a/test/support/accounts/team.ex b/test/support/accounts/team.ex deleted file mode 100644 index 4a79269..0000000 --- a/test/support/accounts/team.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule FactoryEx.Support.Accounts.Team do - @moduledoc false - - use Ecto.Schema - - import Ecto.Changeset, only: [cast: 3, validate_required: 2] - - alias FactoryEx.Support.Accounts.{User, Team, TeamOrganization} - - schema "teams" do - field :name, :string - - has_many :users, User - belongs_to :team_organization, TeamOrganization - end - - @required_params [:name] - - def changeset(%Team{} = user, attrs \\ %{}) do - user - |> cast(attrs, @required_params) - |> validate_required(@required_params) - end -end - - diff --git a/test/support/accounts/team_organization.ex b/test/support/accounts/team_organization.ex deleted file mode 100644 index 4670c5f..0000000 --- a/test/support/accounts/team_organization.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule FactoryEx.Support.Accounts.TeamOrganization do - @moduledoc false - - use Ecto.Schema - - import Ecto.Changeset, only: [cast: 3, validate_required: 2] - - alias FactoryEx.Support.Accounts.{Team, TeamOrganization} - - schema "team_organizations" do - field :name, :string - - has_many :team, Team - end - - @required_params [:name] - - def changeset(%TeamOrganization{} = user, attrs \\ %{}) do - user - |> cast(attrs, @required_params) - |> validate_required(@required_params) - end -end diff --git a/test/support/accounts/user.ex b/test/support/accounts/user.ex deleted file mode 100644 index 3a8d313..0000000 --- a/test/support/accounts/user.ex +++ /dev/null @@ -1,47 +0,0 @@ -defmodule FactoryEx.Support.Accounts.User do - @moduledoc false - - use Ecto.Schema - - import Ecto.Changeset, only: [cast: 3, validate_required: 2, update_change: 3, validate_length: 3] - - alias FactoryEx.Support.Accounts.{User, Role, Team, Label} - - @username_min 3 - @username_max 15 - @email_max_length 255 - - schema "account_users" do - field :name, :string - field :email, :string - field :email_updated_at, :utc_datetime_usec - field :location, :string - field :gender, :string - field :birthday, :date - - belongs_to :role, Role - belongs_to :team, Team - - many_to_many :labels, Label, join_through: "account_user_labels" - - timestamps(type: :utc_datetime_usec) - end - - @required_params [:email] - @available_params [ - :email_updated_at, - :name, - :birthday, - :location, - :gender | @required_params - ] - - def changeset(%User{} = user, attrs \\ %{}) do - user - |> cast(attrs, @available_params) - |> validate_required(@required_params) - |> update_change(:location, &String.upcase/1) - |> validate_length(:name, min: @username_min, max: @username_max) - |> validate_length(:email, max: @email_max_length) - end -end diff --git a/test/support/datacase.ex b/test/support/datacase.ex new file mode 100644 index 0000000..f98930d --- /dev/null +++ b/test/support/datacase.ex @@ -0,0 +1,62 @@ +defmodule FactoryEx.DataCase do + @moduledoc """ + This module defines the setup for tests requiring + access to the application's data layer. + + You may define functions here to be used as helpers in + your tests. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `use FactoryEx.DataCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + using do + quote do + alias FactoryEx.Support.Repo + + import Ecto + import Ecto.Changeset + import Ecto.Query + import FactoryEx.DataCase + end + end + + setup tags do + setup_sandbox(tags) + :ok + end + + def setup_sandbox(tags) do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(FactoryEx.Support.Repo) + + unless tags[:async] do + Ecto.Adapters.SQL.Sandbox.mode(FactoryEx.Support.Repo, {:shared, self()}) + end + end + + @doc """ + A helper that transforms changeset errors into a map of messages. + + assert {:error, changeset} = Accounts.create_user(%{password: "short"}) + assert "password is too short" in errors_on(changeset).password + assert %{password: ["password is too short"]} = errors_on(changeset) + + """ + def errors_on(changeset) do + Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> + Regex.replace(~r"%{(\w+)}", message, fn _, key -> + key_string_to_atom = String.to_existing_atom(key) + + opts + |> Keyword.get(key_string_to_atom, key) + |> to_string() + end) + end) + end +end diff --git a/test/support/factory/accounts/label.ex b/test/support/factory/accounts/label.ex new file mode 100644 index 0000000..30fbe95 --- /dev/null +++ b/test/support/factory/accounts/label.ex @@ -0,0 +1,17 @@ +defmodule FactoryEx.Support.Factory.Accounts.Label do + @moduledoc """ + Account Test Factory + """ + @behaviour FactoryEx + + @impl FactoryEx + def schema, do: FactoryEx.Support.Schema.Accounts.Label + + @impl FactoryEx + def repo, do: FactoryEx.Support.Repo + + @impl FactoryEx + def build(attrs \\ %{}) do + Map.merge(%{label: Faker.Lorem.word()}, attrs) + end +end diff --git a/test/support/factory/accounts/role.ex b/test/support/factory/accounts/role.ex new file mode 100644 index 0000000..12cbce1 --- /dev/null +++ b/test/support/factory/accounts/role.ex @@ -0,0 +1,17 @@ +defmodule FactoryEx.Support.Factory.Accounts.Role do + @moduledoc """ + Account Test Factory + """ + @behaviour FactoryEx + + @impl FactoryEx + def schema, do: FactoryEx.Support.Schema.Accounts.Role + + @impl FactoryEx + def repo, do: FactoryEx.Support.Repo + + @impl FactoryEx + def build(attrs \\ %{}) do + Map.merge(%{code: Faker.Team.name()}, attrs) + end +end diff --git a/test/support/factory/accounts/team.ex b/test/support/factory/accounts/team.ex new file mode 100644 index 0000000..1242c85 --- /dev/null +++ b/test/support/factory/accounts/team.ex @@ -0,0 +1,17 @@ +defmodule FactoryEx.Support.Factory.Accounts.Team do + @moduledoc """ + Account Test Factory + """ + @behaviour FactoryEx + + @impl FactoryEx + def schema, do: FactoryEx.Support.Schema.Accounts.Team + + @impl FactoryEx + def repo, do: FactoryEx.Support.Repo + + @impl FactoryEx + def build(attrs \\ %{}) do + Map.merge(%{name: Faker.Company.name()}, attrs) + end +end diff --git a/test/support/factory/accounts/team_organization.ex b/test/support/factory/accounts/team_organization.ex new file mode 100644 index 0000000..af575e9 --- /dev/null +++ b/test/support/factory/accounts/team_organization.ex @@ -0,0 +1,17 @@ +defmodule FactoryEx.Support.Factory.Accounts.TeamOrganization do + @moduledoc """ + Account Test Factory + """ + @behaviour FactoryEx + + @impl FactoryEx + def schema, do: FactoryEx.Support.Schema.Accounts.TeamOrganization + + @impl FactoryEx + def repo, do: FactoryEx.Support.Repo + + @impl FactoryEx + def build(attrs \\ %{}) do + Map.merge(%{name: Faker.Company.name()}, attrs) + end +end diff --git a/test/support/factory/accounts/user.ex b/test/support/factory/accounts/user.ex new file mode 100644 index 0000000..ddefaf1 --- /dev/null +++ b/test/support/factory/accounts/user.ex @@ -0,0 +1,23 @@ +defmodule FactoryEx.Support.Factory.Accounts.User do + @moduledoc """ + Account Test Factory + """ + @behaviour FactoryEx + + @impl FactoryEx + def schema, do: FactoryEx.Support.Schema.Accounts.User + + @impl FactoryEx + def repo, do: FactoryEx.Support.Repo + + @impl FactoryEx + def build(attrs \\ %{}) do + Map.merge(%{ + name: Faker.Person.name(), + email: Faker.Internet.email(), + gender: Enum.random(~w[male female]), + location: "someplace", + birthday: ~D[1992-10-04] + }, attrs) + end +end diff --git a/test/support/accounts/label.ex b/test/support/schema/accounts/label.ex similarity index 53% rename from test/support/accounts/label.ex rename to test/support/schema/accounts/label.ex index fed707e..80e1942 100644 --- a/test/support/accounts/label.ex +++ b/test/support/schema/accounts/label.ex @@ -1,24 +1,21 @@ -defmodule FactoryEx.Support.Accounts.Label do +defmodule FactoryEx.Support.Schema.Accounts.Label do @moduledoc false use Ecto.Schema import Ecto.Changeset, only: [cast: 3, validate_required: 2] - alias FactoryEx.Support.Accounts.Label + alias FactoryEx.Support.Schema.Accounts.Label schema "account_labels" do - field :label, :string + field(:label, :string) end @required_params [:label] def changeset(%Label{} = user, attrs \\ %{}) do user - |> cast(attrs, @required_params) - |> validate_required(@required_params) + |> cast(attrs, @required_params) + |> validate_required(@required_params) end end - - - diff --git a/test/support/schema/accounts/role.ex b/test/support/schema/accounts/role.ex new file mode 100644 index 0000000..605209f --- /dev/null +++ b/test/support/schema/accounts/role.ex @@ -0,0 +1,23 @@ +defmodule FactoryEx.Support.Schema.Accounts.Role do + @moduledoc false + + use Ecto.Schema + + import Ecto.Changeset, only: [cast: 3, validate_required: 2] + + alias FactoryEx.Support.Schema.Accounts.{Role, User} + + schema "account_roles" do + field(:code, :string) + + has_many(:users, User) + end + + @required_params [:code] + + def changeset(%Role{} = user, attrs \\ %{}) do + user + |> cast(attrs, @required_params) + |> validate_required(@required_params) + end +end diff --git a/test/support/schema/accounts/team.ex b/test/support/schema/accounts/team.ex new file mode 100644 index 0000000..d85d857 --- /dev/null +++ b/test/support/schema/accounts/team.ex @@ -0,0 +1,24 @@ +defmodule FactoryEx.Support.Schema.Accounts.Team do + @moduledoc false + + use Ecto.Schema + + import Ecto.Changeset, only: [cast: 3, validate_required: 2] + + alias FactoryEx.Support.Schema.Accounts.{Team, TeamOrganization, User} + + schema "teams" do + field(:name, :string) + + has_many(:users, User) + belongs_to(:team_organization, TeamOrganization) + end + + @required_params [:name] + + def changeset(%Team{} = user, attrs \\ %{}) do + user + |> cast(attrs, @required_params) + |> validate_required(@required_params) + end +end diff --git a/test/support/schema/accounts/team_organization.ex b/test/support/schema/accounts/team_organization.ex new file mode 100644 index 0000000..3662e4c --- /dev/null +++ b/test/support/schema/accounts/team_organization.ex @@ -0,0 +1,23 @@ +defmodule FactoryEx.Support.Schema.Accounts.TeamOrganization do + @moduledoc false + + use Ecto.Schema + + import Ecto.Changeset, only: [cast: 3, validate_required: 2] + + alias FactoryEx.Support.Schema.Accounts.{Team, TeamOrganization} + + schema "team_organizations" do + field(:name, :string) + + has_many(:teams, Team) + end + + @required_params [:name] + + def changeset(%TeamOrganization{} = user, attrs \\ %{}) do + user + |> cast(attrs, @required_params) + |> validate_required(@required_params) + end +end diff --git a/test/support/schema/accounts/user.ex b/test/support/schema/accounts/user.ex new file mode 100644 index 0000000..c00b6be --- /dev/null +++ b/test/support/schema/accounts/user.ex @@ -0,0 +1,48 @@ +defmodule FactoryEx.Support.Schema.Accounts.User do + @moduledoc false + + use Ecto.Schema + + import Ecto.Changeset, + only: [cast: 3, validate_required: 2, update_change: 3, validate_length: 3] + + alias FactoryEx.Support.Schema.Accounts.{Label, Role, Team, User} + + @username_min 3 + @username_max 30 + @email_max_length 255 + + schema "account_users" do + field(:name, :string) + field(:email, :string) + field(:email_updated_at, :utc_datetime_usec) + field(:location, :string) + field(:gender, :string) + field(:birthday, :date) + + belongs_to(:role, Role) + belongs_to(:team, Team) + + many_to_many(:labels, Label, join_through: "account_user_labels") + + timestamps(type: :utc_datetime_usec) + end + + @required_params [:email] + @available_params [ + :email_updated_at, + :name, + :birthday, + :location, + :gender | @required_params + ] + + def changeset(%User{} = user, attrs \\ %{}) do + user + |> cast(attrs, @available_params) + |> validate_required(@required_params) + |> update_change(:location, &String.upcase/1) + |> validate_length(:name, min: @username_min, max: @username_max) + |> validate_length(:email, max: @email_max_length) + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e..1c32982 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,3 @@ ExUnit.start() + +{:ok, _} = FactoryEx.Support.Repo.start_link() From 0e7dbbcb7c99293d5564d85e7b4a60af37971a3e Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Fri, 31 Mar 2023 09:01:40 -0700 Subject: [PATCH 02/26] cleanup --- .tool-versions | 2 - config/config.exs | 6 +- lib/factory_ex/association_builder.ex | 7 +- lib/factory_ex/utils.ex | 6 +- lib/mix/factory_ex_helpers.ex | 80 +++------ lib/mix/tasks/gen.ex | 242 ++++++++++---------------- 6 files changed, 129 insertions(+), 214 deletions(-) delete mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index ee4c41c..0000000 --- a/.tool-versions +++ /dev/null @@ -1,2 +0,0 @@ -erlang 25.1 -elixir 1.13.4-otp-25 diff --git a/config/config.exs b/config/config.exs index 13f7881..f27aa2c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -3,9 +3,9 @@ import Config config :factory_ex, ecto_repos: [FactoryEx.Support.Repo] config :factory_ex, :sql_sandbox, true config :factory_ex, FactoryEx.Support.Repo, - username: "postgres", - password: "postgres", + username: System.get_env("POSTGRES_USER") || "postgres", + password: System.get_env("POSTGRES_PASSWORD") || "postgres", database: "factory_ex_test", hostname: "localhost", pool: Ecto.Adapters.SQL.Sandbox, - pool_size: 5 + pool_size: String.to_integer(System.get_env("POSTGRES_POOL_SIZE", "10")) diff --git a/lib/factory_ex/association_builder.ex b/lib/factory_ex/association_builder.ex index 52d7f09..04d490b 100644 --- a/lib/factory_ex/association_builder.ex +++ b/lib/factory_ex/association_builder.ex @@ -16,7 +16,7 @@ defmodule FactoryEx.AssociationBuilder do - If the `owner_key` of the association's schema field is not set, the factory's `build/1` function will be invoked with the field's existing value. If the owner key is set the - field is skipped and the existing value will be kept. Any existing paramaters not passed + field is skipped and the existing value will be kept. Any existing parameters not passed as a relational key will be kept. If this behaviour is not desired you can set the `check_owner_field?` option to `false` and the parameters will be generated when the owner key is set. @@ -118,8 +118,9 @@ defmodule FactoryEx.AssociationBuilder do if check_owner_key? and owner_key_is_set?(assoc, params) do Map.get(params, field) else - params = Map.get(params, field, [%{}]) - Enum.map(params, fn params -> factory_build(queryable, params, assoc_fields, check_owner_key?) end) + params + |> Map.get(field, [%{}]) + |> Enum.map(&factory_build(queryable, &1, assoc_fields, check_owner_key?)) end end diff --git a/lib/factory_ex/utils.ex b/lib/factory_ex/utils.ex index 4193bb3..d254745 100644 --- a/lib/factory_ex/utils.ex +++ b/lib/factory_ex/utils.ex @@ -148,7 +148,7 @@ defmodule FactoryEx.Utils do ## Examples - iex> FactoryEx.TermExpander.expand_count_tuples(%{hello: {2, %{world: {2, %{foo: :bar}}}}}) + iex> FactoryEx.Utils.expand_count_tuples(%{hello: {2, %{world: {2, %{foo: :bar}}}}}) %{ hello: [ %{world: [%{foo: :bar}, %{foo: :bar}]}, @@ -156,10 +156,10 @@ defmodule FactoryEx.Utils do ] } - iex> FactoryEx.TermExpander.expand_count_tuples(%{hello: [%{foo: "bar"}, {2, %{}}]}) + iex> FactoryEx.Utils.expand_count_tuples(%{hello: [%{foo: "bar"}, {2, %{}}]}) %{hello: [%{foo: "bar"}, %{}, %{}]} - iex> FactoryEx.TermExpander.expand_count_tuples(%{hello: [%{foo: {1, %{}}}, {1, %{qux: {1, %{bux: "hello world"}}}}]}) + iex> FactoryEx.Utils.expand_count_tuples(%{hello: [%{foo: {1, %{}}}, {1, %{qux: {1, %{bux: "hello world"}}}}]}) %{hello: [%{foo: [%{}]}, %{qux: [%{bux: "hello world"}]}]} """ @spec expand_count_tuples(map() | list()) :: map() diff --git a/lib/mix/factory_ex_helpers.ex b/lib/mix/factory_ex_helpers.ex index e161b3d..82a67e5 100644 --- a/lib/mix/factory_ex_helpers.ex +++ b/lib/mix/factory_ex_helpers.ex @@ -5,31 +5,22 @@ defmodule Mix.FactoryExHelpers do def ensure_not_in_umbrella!(command) do if Mix.Project.umbrella?() do - Mix.raise( - "mix #{command} must be invoked from within your *_web application root directory" - ) + Mix.raise("mix #{command} must be invoked from within your *_web application root directory") end end def string_to_module(module_string) do Module.safe_concat([module_string]) - rescue - ArgumentError -> - reraise to_string( - IO.ANSI.format([ - :red, - "Module ", - :bright, - module_string, - :reset, - :red, - " doesn't exist", - :reset - ]) - ), - __STACKTRACE__ + + rescue + ArgumentError -> + reraise to_string(IO.ANSI.format([ + :red, "Module ", :bright, module_string, + :reset, :red, " doesn't exist", :reset + ])), __STACKTRACE__ end + def schema_primary_key(ecto_schema) do ecto_schema.__schema__(:primary_key) end @@ -58,62 +49,41 @@ defmodule Mix.FactoryExHelpers do def schema_association_module(ecto_schema, relation_name) do case schema_association(ecto_schema, relation_name) do - %{queryable: queryable} -> - queryable - + %{queryable: queryable} -> queryable _ -> - raise to_string( - IO.ANSI.format( - [ - :red, - :bright, - to_string(relation_name), - :reset, - :red, - "doesn't exist on ", - :bright, - inspect(ecto_schema), - :reset, - :red, - ", check your config for this key" - ], - true - ) - ) + raise to_string(IO.ANSI.format([ + :red, :bright, to_string(relation_name), :reset, + :red, "doesn't exist on ", :bright, inspect(ecto_schema), :reset, + :red, ", check your config for this key" + ], true)) end end def schema_module(ecto_schema) do ecto_schema - |> Module.split() - |> List.last() + |> Module.split + |> List.last end def schema_module_resource_name(ecto_schema) do ecto_schema - |> schema_module - |> Macro.underscore() + |> schema_module + |> Macro.underscore end def fields_intersection(fields_a, fields_b) do fields_a - |> MapSet.new() - |> MapSet.intersection(MapSet.new(fields_b)) - |> MapSet.to_list() + |> MapSet.new + |> MapSet.intersection(MapSet.new(fields_b)) + |> MapSet.to_list end def schema_relationship_list?(ecto_schema, relation_name) do case schema_association(ecto_schema, relation_name) do - %{cardinality: :one} -> - false - - %{cardinality: :many} -> - true - + %{cardinality: :one} -> false + %{cardinality: :many} -> true assication_record -> - Logger.error( - "[PhoenixConfig.EctoUtils] Error checking if schema #{ecto_schema} relationship #{relation_name} is a list\n#{inspect(assication_record)}" - ) + Logger.error("[PhoenixConfig.EctoUtils] Error checking if schema #{ecto_schema} relationship #{relation_name} is a list\n#{inspect assication_record}") false end diff --git a/lib/mix/tasks/gen.ex b/lib/mix/tasks/gen.ex index 59dd8b1..c63ab72 100644 --- a/lib/mix/tasks/gen.ex +++ b/lib/mix/tasks/gen.ex @@ -25,66 +25,50 @@ defmodule Mix.Tasks.FactoryEx.Gen do ] @faker_functions (case :application.get_key(:faker, :modules) do - {:ok, modules} -> - modules - |> Enum.filter(fn module -> - module_level = - module - |> inspect - |> String.codepoints() - |> Enum.filter(&(&1 === ".")) - |> length - - module_level === 1 - end) - |> Enum.map(fn module -> - functions = - module.__info__(:functions) - |> Enum.filter(fn {_name, arity} -> arity === 0 end) - |> Enum.map(fn {name, _arity} -> name end) - - {module, functions} - end) - |> Enum.filter(fn {_module, functions} -> length(functions) > 0 end) - - e -> - throw( - "Some weird error happened when trying to find faker functions\n#{inspect(e, pretty: true)}" - ) - end) + {:ok, modules} -> + modules + |> Enum.filter(fn module -> + module_level = module + |> inspect + |> String.codepoints + |> Enum.filter(&(&1 === ".")) + |> length + + module_level === 1 + end) + |> Enum.map(fn module -> + functions = module.__info__(:functions) + |> Enum.filter(fn {_name, arity} -> arity === 0 end) + |> Enum.map(fn {name, _arity} -> name end) + + {module, functions} + end) + |> Enum.filter(fn {_module, functions} -> length(functions) > 0 end) + + e -> + throw "Some weird error happened when trying to find faker functions\n#{inspect e, pretty: true}" + end) def run(args) do Mix.Task.run("app.config", []) FactoryExHelpers.ensure_not_in_umbrella!("factory_ex.gen.factory") - {opts, extra_args, _} = - OptionParser.parse(args, - switches: [ - dirname: :string, - app_name: :string, - force: :boolean, - quiet: :boolean, - repo: :string - ] - ) + {opts, extra_args, _} = OptionParser.parse(args, + switches: [ + dirname: :string, + app_name: :string, + force: :boolean, + quiet: :boolean, + repo: :string + ] + ) if opts[:app_name] && opts[:dirname] do - raise to_string( - IO.ANSI.format([ - :red, - "Only one of ", - :bright, - "app_name", - :reset, - :red, - " or ", - :bright, - "dirname", - :reset, - :red, - " should be supplied" - ]) - ) + raise to_string(IO.ANSI.format([ + :red, "Only one of ", :bright, "app_name", :reset, + :red, " or ", :bright, "dirname", :reset, + :red, " should be supplied" + ])) end if validate_repo?(opts[:repo]) do @@ -102,59 +86,40 @@ defmodule Mix.Tasks.FactoryEx.Gen do true else - Mix.shell().error( - to_string( - IO.ANSI.format([ - :reset, - :red, - "Must provide ", - :bright, - "--repo", - :reset, - :red, - " when using factory_ex.gen", - :reset - ]) - ) - ) + Mix.shell().error(to_string(IO.ANSI.format([ + :reset, :red, "Must provide ", :bright, "--repo", :reset, + :red, " when using factory_ex.gen", :reset + ]))) false end end def ensure_schema_counter_start_added(opts) do - directory = - cond do - opts[:dirname] -> opts[:dirname] - opts[:app_name] -> Path.expand(Path.join(["../", opts[:app_name]])) - true -> "." - end + directory = cond do + opts[:dirname] -> opts[:dirname] + opts[:app_name] -> Path.expand(Path.join(["../", opts[:app_name]])) + true -> "." + end "#{directory}/**/test_helper.exs" - |> Path.wildcard() - |> Enum.each(fn path -> - path = Path.expand(path) - - contents = File.read!(path) + |> Path.wildcard + |> Enum.each(fn path -> + path = Path.expand(path) - if not String.contains?(contents, "FactoryEx.SchemaCounter.start()") do - path = Path.relative_to_cwd(path) + contents = File.read!(path) - Mix.shell().info([ - :green, - "* injecting FactoryEx.SchemaCounter.start() into ", - :reset, - path - ]) + if not String.contains?(contents, "FactoryEx.SchemaCounter.start()") do + path = Path.relative_to_cwd(path) + Mix.shell().info([:green, "* injecting FactoryEx.SchemaCounter.start() into ", :reset, path]) - File.write!(path, contents <> "\nFactoryEx.SchemaCounter.start()", opts) - end - end) + File.write!(path, contents <> "\nFactoryEx.SchemaCounter.start()", opts) + end + end) end def generate_factory(ecto_schema, repo, opts) do - schema_fields = - ecto_schema + schema_fields = ecto_schema |> FactoryExHelpers.schema_fields() |> Kernel.--(FactoryExHelpers.schema_primary_key(ecto_schema) ++ @blacklist_fields) |> FactoryExHelpers.with_field_types(ecto_schema) @@ -167,17 +132,11 @@ defmodule Mix.Tasks.FactoryEx.Gen do end defp schema_factory_path(ecto_schema, opts) do - dirname = - cond do - opts[:dirname] -> - opts[:dirname] - - opts[:app_name] -> - Path.expand(Path.join(["../", opts[:app_name], "test/support/factory"])) - - true -> - Path.expand("./test/support/factory/") - end + dirname = cond do + opts[:dirname] -> opts[:dirname] + opts[:app_name] -> Path.expand(Path.join(["../", opts[:app_name], "test/support/factory"])) + true -> Path.expand("./test/support/factory/") + end [context, schema] = ecto_schema |> inspect |> String.split(".") |> Enum.take(-2) dirname = Path.join(dirname, Macro.underscore(context)) @@ -189,8 +148,8 @@ defmodule Mix.Tasks.FactoryEx.Gen do end dirname - |> Path.join(file_name) - |> Path.relative_to_cwd() + |> Path.join(file_name) + |> Path.relative_to_cwd end defp factory_template(ecto_schema, repo, schema_fields, _opts) do @@ -216,12 +175,9 @@ defmodule Mix.Tasks.FactoryEx.Gen do defp ecto_schema_factory_module(ecto_schema) do [root_module | nested_modules] = ecto_schema |> inspect |> String.split(".") - - other_modules = - nested_modules + other_modules = nested_modules |> Enum.join(".") - # This is just to support tests - |> String.replace(~r/^Support\./, "") + |> String.replace(~r/^Support\./, "") # This is just to support tests "#{root_module}.Support.Factory.#{other_modules}" end @@ -231,8 +187,7 @@ defmodule Mix.Tasks.FactoryEx.Gen do end defp build_random_field(:integer, field, ecto_schema) do - schema_name = - ecto_schema + schema_name = ecto_schema |> inspect() |> String.split(".") |> Enum.map_join("_", &Macro.underscore/1) @@ -251,9 +206,7 @@ defmodule Mix.Tasks.FactoryEx.Gen do case find_faker_function_with_type(field, :string) do {module, function} -> "\"\#{#{inspect(module)}.#{function}()\}_\#{FactoryEx.SchemaCounter.next(\"#{field_name}\")\}\"" - - nil -> - "to_string(FactoryEx.SchemaCounter.next(\"#{field_name}\"))" + nil -> "to_string(FactoryEx.SchemaCounter.next(\"#{field_name}\"))" end end @@ -277,12 +230,8 @@ defmodule Mix.Tasks.FactoryEx.Gen do "Faker.Date.backward(Enum.random(100..400))" end - defp build_random_field( - {:parameterized, Ecto.Enum, %{mappings: mappings}}, - _field, - _ecto_schema - ) do - enum_list = mappings |> Keyword.keys() |> Enum.map_join(", ", &":#{&1}") + defp build_random_field({:parameterized, Ecto.Enum, %{mappings: mappings}}, _field, _ecto_schema) do + enum_list = mappings |> Keyword.keys |> Enum.map_join(", ", &(":#{&1}")) "Enum.random([#{enum_list}])" end @@ -291,32 +240,31 @@ defmodule Mix.Tasks.FactoryEx.Gen do defp find_faker_function_with_type(field, type) do field - |> matching_faker_functions - |> Enum.find(fn {module, function} -> - if module not in @faker_mod_blacklist do - faker_fn_return_type = - module - |> apply(function, []) - |> resolve_type - - faker_fn_return_type === type - end - end) + |> matching_faker_functions + |> Enum.find(fn {module, function} -> + if module not in @faker_mod_blacklist do + faker_fn_return_type = module + |> apply(function, []) + |> resolve_type + + faker_fn_return_type === type + end + end) end - defp resolve_type(%NaiveDateTime{}) do + defp resolve_type(%NaiveDateTime{}) do :naive_datetime_usec end - defp resolve_type(%DateTime{}) do + defp resolve_type(%DateTime{}) do :datetime_usec end - defp resolve_type(%Date{}) do + defp resolve_type(%Date{}) do :date end - defp resolve_type(%Time{}) do + defp resolve_type(%Time{}) do :time end @@ -346,18 +294,16 @@ defmodule Mix.Tasks.FactoryEx.Gen do def matching_faker_functions(field) do @faker_functions - |> Enum.flat_map(fn {module, functions} -> - Enum.map(functions, fn function_name -> - module_name = module |> inspect |> String.replace("Faker", "") - - score = - String.jaro_distance(to_string(function_name), to_string(field)) + - String.jaro_distance(to_string(module_name), to_string(field)) / 2 - - {module, function_name, score} + |> Enum.flat_map(fn {module, functions} -> + Enum.map(functions, fn function_name -> + module_name = module |> inspect |> String.replace("Faker", "") + score = String.jaro_distance(to_string(function_name), to_string(field)) + + (String.jaro_distance(to_string(module_name), to_string(field)) / 2) + + {module, function_name, score} + end) end) - end) - |> Enum.sort_by(fn {_mod, _fn_name, score} -> score end, :desc) - |> Enum.map(fn {module, fn_name, _distance} -> {module, fn_name} end) + |> Enum.sort_by(fn {_mod, _fn_name, score} -> score end, :desc) + |> Enum.map(fn {module, fn_name, _distance} -> {module, fn_name} end) end end From 50cf31b5d4dadf8d9462b82fae35b43b898d9bd4 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Fri, 31 Mar 2023 09:23:55 -0700 Subject: [PATCH 03/26] add insert test --- test/factory_ex_test.exs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/factory_ex_test.exs b/test/factory_ex_test.exs index a9362e5..583ad91 100644 --- a/test/factory_ex_test.exs +++ b/test/factory_ex_test.exs @@ -140,5 +140,21 @@ defmodule FactoryExTest do %{}, relational: [teams: [users: [:role, :labels]]] ) + + assert %FactoryEx.Support.Schema.Accounts.TeamOrganization{ + teams: [ + %{ + users: [ + %FactoryEx.Support.Schema.Accounts.User{}, + %FactoryEx.Support.Schema.Accounts.User{} + ] + } + ] + } = + FactoryEx.insert!( + FactoryEx.Support.Factory.Accounts.TeamOrganization, + %{teams: [%{users: {2, %{}}}]}, + relational: [teams: [:users]] + ) end end From 17e190fe28b4920a2902c5374d36da124a250cc2 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Fri, 31 Mar 2023 17:36:05 -0700 Subject: [PATCH 04/26] only handle eum for expand count tuples --- lib/factory_ex/utils.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/factory_ex/utils.ex b/lib/factory_ex/utils.ex index d254745..0e608fc 100644 --- a/lib/factory_ex/utils.ex +++ b/lib/factory_ex/utils.ex @@ -163,7 +163,9 @@ defmodule FactoryEx.Utils do %{hello: [%{foo: [%{}]}, %{qux: [%{bux: "hello world"}]}]} """ @spec expand_count_tuples(map() | list()) :: map() - def expand_count_tuples(enum), do: enum |> Enum.map(&transform/1) |> Map.new() + def expand_count_tuples(enum) when is_map(enum) or is_list(enum), do: enum |> Enum.map(&transform/1) |> Map.new() + + def expand_count_tuples(val), do: val defp expand_many_count_tuples(count, attrs), do: Enum.map(1..count, fn _ -> expand_count_tuples(attrs) end) From e02da663ef6e977fe5562c9afd19f50327553ef3 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Sat, 1 Jul 2023 14:31:49 -0700 Subject: [PATCH 05/26] change task to cache and remove application --- config/config.exs | 10 -- lib/factory_ex/application.ex | 35 ---- lib/factory_ex/association_builder.ex | 2 +- lib/factory_ex/factory_cache.ex | 233 ++++++++++++++++++++++++++ lib/factory_ex/factory_store.ex | 138 --------------- lib/factory_ex/utils.ex | 18 ++ mix.exs | 6 +- mix.lock | 12 +- test/test_helper.exs | 16 +- 9 files changed, 281 insertions(+), 189 deletions(-) delete mode 100644 lib/factory_ex/application.ex create mode 100644 lib/factory_ex/factory_cache.ex delete mode 100644 lib/factory_ex/factory_store.ex diff --git a/config/config.exs b/config/config.exs index f27aa2c..becde76 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,11 +1 @@ import Config - -config :factory_ex, ecto_repos: [FactoryEx.Support.Repo] -config :factory_ex, :sql_sandbox, true -config :factory_ex, FactoryEx.Support.Repo, - username: System.get_env("POSTGRES_USER") || "postgres", - password: System.get_env("POSTGRES_PASSWORD") || "postgres", - database: "factory_ex_test", - hostname: "localhost", - pool: Ecto.Adapters.SQL.Sandbox, - pool_size: String.to_integer(System.get_env("POSTGRES_POOL_SIZE", "10")) diff --git a/lib/factory_ex/application.ex b/lib/factory_ex/application.ex deleted file mode 100644 index f173c91..0000000 --- a/lib/factory_ex/application.ex +++ /dev/null @@ -1,35 +0,0 @@ -defmodule FactoryEx.Application do - @moduledoc false - - use Application - - @impl true - def start(_, _) do - FactoryEx.SchemaCounter.start() - - children = [ - FactoryEx.FactoryStore - ] - - opts = [strategy: :one_for_one, name: FactoryEx.Supervisor] - Supervisor.start_link(children, opts) - end - - def apps_that_depend_on(dep) do - :application.loaded_applications() - |> Enum.reduce([], fn {app, _, _}, acc -> - deps = Application.spec(app)[:applications] - (dep in deps && acc ++ [app]) || acc - end) - end - - def find_app_modules(app, prefix) do - case :application.get_key(app, :modules) do - {:ok, modules} -> - prefix = Module.split(prefix) - Enum.filter(modules, &(&1 |> Module.split() |> FactoryEx.Utils.sublist?(prefix))) - - _ -> raise "modules not found for app #{inspect(app)}." - end - end -end diff --git a/lib/factory_ex/association_builder.ex b/lib/factory_ex/association_builder.ex index 04d490b..ccc7b6f 100644 --- a/lib/factory_ex/association_builder.ex +++ b/lib/factory_ex/association_builder.ex @@ -147,7 +147,7 @@ defmodule FactoryEx.AssociationBuilder do end defp factory_build(queryable, params, assoc_fields, check_owner_key?) do - parent = FactoryEx.FactoryStore.build_params(queryable, params) + parent = FactoryEx.FactoryCache.build_params(queryable, params) assoc = convert_fields_to_params(queryable, params, assoc_fields, check_owner_key?) Map.merge(parent, assoc) diff --git a/lib/factory_ex/factory_cache.ex b/lib/factory_ex/factory_cache.ex new file mode 100644 index 0000000..5c97b00 --- /dev/null +++ b/lib/factory_ex/factory_cache.ex @@ -0,0 +1,233 @@ +defmodule FactoryEx.FactoryCache do + @moduledoc """ + KV store that maps Ecto.Schema modules keys to Factory module values. + + + # Getting Started + + To use this cache it must first be started. It can be added a child to your + application supervisor or add the following snippet below to your test_helper.exs. + + ```elixir + # test_helper.exs + Cache.start_link([FactoryEx.FactoryCache]) + ``` + + Once the cache is started the store then needs to be created. + + ```elixir + FactoryEx.FactoryCache.setup() + :ok + ``` + + This function finds your factory modules, maps them to their schemas and stores + the result. This also works with umbrella apps out of the box. + + We can now view the modules in the store with `FactoryEx.FactoryCache.get_store/0`. + By default the schemas in factory_ex are shown. Here's an example: + + ```elixir + FactoryEx.FactoryCache.get_store() + {:ok, + %{ + FactoryEx.Support.Schema.Accounts.Label => FactoryEx.Support.Factory.Accounts.Label, + FactoryEx.Support.Schema.Accounts.Role => FactoryEx.Support.Factory.Accounts.Role, + FactoryEx.Support.Schema.Accounts.Team => FactoryEx.Support.Factory.Accounts.Team, + FactoryEx.Support.Schema.Accounts.TeamOrganization => FactoryEx.Support.Factory.Accounts.TeamOrganization, + FactoryEx.Support.Schema.Accounts.User => FactoryEx.Support.Factory.Accounts.User + }} + ``` + + Once the store is created you can build parameters for a given schema with `FactoryEx.FactoryCache.build_params/2`. + Let's go through an example using a schema in the store: + + ```elixir + FactoryEx.FactoryCache.build_params(FactoryEx.Support.Schema.Accounts.User, %{location: "custom_location"}) + %{ + birthday: ~D[1992-10-04], + email: "tyrese_welch@beier.org", + gender: "male", + location: "custom_location", + name: "Elisa Abbott" + } + ``` + + In the example above we called `FactoryEx.FactoryCache.build_params/2` with the schema + `FactoryEx.Support.Factory.Accounts.User` and some parameters. In the store the schema + key has the value of `FactoryEx.Support.Factory.Accounts.User`. This in turn calls + the factory's callback function `build/1` called with the parameters. + + ## Requirements + + - Your application must have `factory_ex` as a dependency in `mix.exs`. + - Your application defines a factory module for each schema used as a relational key + - Your module contains the prefix `Factory`, ie. YourApp.Support.Factory.Schemas.Schema. + - Your factory module defines the factory_ex `schema/0` callback function. + + Note: In umbrella applications the requirements are per application. + + ## Factory Module Prefix + + If your application's factory modules do not use the prefix `Factory` or you want to change + which factory modules are loaded during tests you can configure the module prefix option at + *compile* time with the following config: + + ```elixir + config :factory_ex, :factory_module_prefix, Factory + ``` + + ## Test Isolation + + The factories can be isolated per test by enabling the sandbox through the config at *compile* time: + + ```elixir + config :factory_ex, :sandbox_factory_cache?, true + ``` + + This method relies on `Cache.SandboxRegistry` which has to be started before your tests: + + ```elixir + # test_helper.exs + Cache.SandboxRegistry.start_link() + ``` + + Once the sandbox registry is started you must then register the cache and put a value for + the store for each test. This can be done in a setup block: + + ```elixir + # your test file + defmodule YourModuleTest do + setup do + Cache.SandboxRegistry.register_caches(FactoryEx.FactoryCache) + + FactoryEx.FactoryCache.put_store(%{ + FactoryEx.Support.Schema.Accounts.User => FactoryEx.Support.Factory.Accounts.User + }) + end + end + ``` + """ + @app :factory_ex + @sandbox_enabled Application.compile_env(@app, :sandbox_factory_cache?, false) + @factory_prefix Application.compile_env(@app, :factory_module_prefix, Factory) + + use Cache, + adapter: Cache.ETS, + name: :factory_ex_factory_store, + sandbox?: @sandbox_enabled, + opts: [] + + @store :store + + @doc "Returns the store map. Raises if no values exist." + @spec fetch_store!() :: map() + def fetch_store! do + {:ok, store} = get_store() + store + end + + @doc "Puts a value in the store." + @spec put_store(map()) :: {:ok, map()} | {:ok, nil} + def put_store(val) do + with :ok <- ensure_cache_started!() do + put(@store, val) + end + end + + @doc "Returns the store." + @spec get_store() :: {:ok, map()} | {:ok, nil} + def get_store() do + with :ok <- ensure_cache_started!(), + {:ok, nil} <- get(@store) do + raise """ + FactoryCache store not found! To fix this error call `FactoryEx.FactoryCache.setup/0`. + """ + end + end + + defp ensure_cache_started! do + if @sandbox_enabled or cache_started?() do + :ok + else + raise """ + FactoryEx.FactoryCache not started! + + Add the following to your test_helper.exs: + + ``` + # test_helper.exs + Cache.start_link([FactoryEx.FactoryCache]) + ``` + """ + end + end + + defp cache_started? do + case :ets.whereis(FactoryEx.FactoryCache.cache_name()) do + :undefined -> false + _ -> true + end + end + + @doc "Builds the store and hydrates the cache." + @spec setup :: :ok + def setup do + put_store(build_store()) + end + + @doc "Builds the store" + @spec build_store :: map() + def build_store do + Enum.reduce( + [@app | FactoryEx.Utils.apps_that_depend_on(@app)], + %{}, + &lookup_factory_modules/2 + ) + end + + defp lookup_factory_modules(app, acc) do + app + |> FactoryEx.Utils.find_app_modules(@factory_prefix) + |> Enum.reduce(acc, &maybe_put_factory_module/2) + end + + defp maybe_put_factory_module(module, acc) do + if FactoryEx.Utils.ensure_function_exported?(module, :schema, 0) do + Map.put(acc, module.schema(), module) + else + acc + end + end + + @doc """ + Returns the result of the `build/1` function of the Factory associated with the given schema. + """ + @spec build_params(module, map) :: map + def build_params(ecto_schema, params \\ %{}) do + fetch_schema_factory!(ecto_schema).build(params) + end + + defp fetch_schema_factory!(ecto_schema) do + case Enum.find(fetch_store!(), fn {k, _} -> k === ecto_schema end) do + nil -> + raise """ + Factory not found for schema '#{inspect(ecto_schema)}'. + + This means the factory module does not exist or it was not loaded. + + To fix this error: + + - Add `factory_ex` as a depedency in `mix.exs` of the application that contains + the schema '#{inspect(ecto_schema)}'. In umbrella applications you must add + `factory_ex` as a dependency to each application that contain factory modules. + + - Create a factory for the schema '#{inspect(ecto_schema)}' + + - Add the prefix '#{inspect(@factory_prefix)}' to the factory module name. For example: + YourApp.#{inspect(@factory_prefix)}.Module. + """ + + {_, factory} -> factory + end + end +end diff --git a/lib/factory_ex/factory_store.ex b/lib/factory_ex/factory_store.ex deleted file mode 100644 index f0fec6c..0000000 --- a/lib/factory_ex/factory_store.ex +++ /dev/null @@ -1,138 +0,0 @@ -defmodule FactoryEx.FactoryStore do - @moduledoc """ - A simple key-value store that maps Ecto.Schema modules to Factory modules. - - ## Requirements - - - Your application defines a factory module for each schema used as a relational key - - Your application has `factory_ex` as a depedency in `mix.exs`. - - Your module contains the prefix `Factory`, ie. YourApp.Support.Factory.Schemas.Schema. - - Your factory module defines the factory_ex `schema/0` callback function. - - Factory modules are stored in an ets table with their schema values as the key. - This allows us to invoke the the appropriate factory module for a given schema. - - Let's look at an example: - - Call `FactoryEx.FactoryStore.all/0` to see all discovered factory modules. - By default the modules defined by `factory_ex` will be displayed. - - ```elixir - FactoryEx.FactoryStore.all - [ - {FactoryEx.Support.Schema.Accounts.User, FactoryEx.Support.Factory.Accounts.User}, - {FactoryEx.Support.Schema.Accounts.TeamOrganization, FactoryEx.Support.Factory.Accounts.TeamOrganization}, - {FactoryEx.Support.Schema.Accounts.Label, FactoryEx.Support.Factory.Accounts.Label}, - {FactoryEx.Support.Schema.Accounts.Role, FactoryEx.Support.Factory.Accounts.Role}, - {FactoryEx.Support.Schema.Accounts.Team, FactoryEx.Support.Factory.Accounts.Team} - ] - ``` - - To build the params for the schema `FactoryEx.Support.Schema.Accounts.User` we call - `FactoryEx.FactoryStore.build_params/2` with the schema module. Arguments are - passed to the factory module's `build/1` callback functions. - - ## Usage Examples - - FactoryEx.FactoryStore.build_params(FactoryEx.Support.Schema.Accounts.User) - %{ - birthday: ~D[1992-10-04], - email: "tyrese_welch@beier.org", - gender: "male", - location: "someplace", - name: "Elisa Abbott" - } - - ## Factory Module Prefix - - If your application's factory modules do not use the prefix `Factory` or you want to change - which factory modules are loaded during tests you can configure the module prefix option at - compile time with the following config: - - ```elixir - config :factory_ex, :factory_module_prefix, Factory - ``` - """ - use Task - - @tab :factory_ex_factory_store - @tab_options [ - :set, - :named_table, - :public, - read_concurrency: true, - write_concurrency: true - ] - - @app :factory_ex - @factory_prefix Application.compile_env(@app, :factory_module_prefix, Factory) - - @doc false - @spec start_link(any) :: {:ok, pid} - def start_link(_) do - Task.start_link(fn -> - :ets.new(@tab, @tab_options) - init() - - Process.hibernate(Function, :identity, []) - end) - end - - defp init do - [@app | FactoryEx.Application.apps_that_depend_on(@app)] - |> Enum.reduce(%{}, &lookup_factory_modules/2) - |> Enum.map(&:ets.insert(@tab, &1)) - end - - defp lookup_factory_modules(app, acc) do - app - |> FactoryEx.Application.find_app_modules(@factory_prefix) - |> Enum.reduce(acc, &maybe_put_factory_module/2) - end - - defp maybe_put_factory_module(module, acc) do - if FactoryEx.Utils.ensure_function_exported?(module, :schema, 0) do - Map.put(acc, module.schema(), module) - else - acc - end - end - - defp fetch_schema_factory!(ecto_schema) do - case @tab |> :ets.lookup(ecto_schema) |> Keyword.get(ecto_schema) do - nil -> - raise """ - Factory not found for schema '#{inspect(ecto_schema)}'. - - This means the factory module does not exist or it was not loaded. - - To fix this error: - - - Add `factory_ex` as a depedency in `mix.exs` of the application that contains - the schema '#{inspect(ecto_schema)}'. In umbrella applications you must add - `factory_ex` as a dependency to each application that contain factory modules. - - - Create a factory for the schema '#{inspect(ecto_schema)}' - - - Add the prefix '#{inspect(@factory_prefix)}' to the factory module name. For example: - YourApp.#{inspect(@factory_prefix)}.Module. - """ - - factory -> factory - end - end - - @doc """ - Returns all factories in the ets table - """ - @spec all() :: list({module, module}) | [] - def all, do: :ets.tab2list(@tab) - - @doc """ - Returns the result of the `build/1` function of the Factory associated with the given schema. - """ - @spec build_params(module, map) :: map - def build_params(ecto_schema, params \\ %{}) do - fetch_schema_factory!(ecto_schema).build(params) - end -end diff --git a/lib/factory_ex/utils.ex b/lib/factory_ex/utils.ex index 0e608fc..8b3057e 100644 --- a/lib/factory_ex/utils.ex +++ b/lib/factory_ex/utils.ex @@ -143,6 +143,24 @@ defmodule FactoryEx.Utils do end end + def apps_that_depend_on(dep) do + :application.loaded_applications() + |> Enum.reduce([], fn {app, _, _}, acc -> + deps = Application.spec(app)[:applications] + (dep in deps && acc ++ [app]) || acc + end) + end + + def find_app_modules(app, prefix) do + case :application.get_key(app, :modules) do + {:ok, modules} -> + prefix = Module.split(prefix) + Enum.filter(modules, &(&1 |> Module.split() |> FactoryEx.Utils.sublist?(prefix))) + + _ -> raise "modules not found for app #{inspect(app)}." + end + end + @doc """ Deep Converts `{count, attrs}` to list of `attrs`. diff --git a/mix.exs b/mix.exs index 3740a54..47c5e88 100644 --- a/mix.exs +++ b/mix.exs @@ -34,8 +34,7 @@ defmodule FactoryEx.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ - extra_applications: [:logger, :postgrex], - mod: {FactoryEx.Application, []} + extra_applications: [:logger, :postgrex] ] end @@ -44,7 +43,8 @@ defmodule FactoryEx.MixProject do [ {:ecto, "~> 3.0"}, {:faker, ">= 0.0.0"}, - {:nimble_options, "~> 0.4"}, + {:nimble_options, "~> 0.5"}, + {:elixir_cache, "~> 0.3.0"}, {:ecto_sql, "~> 3.0", only: [:test, :dev], optional: true}, {:postgrex, "~> 0.16", only: [:test, :dev], optional: true}, {:credo, "~> 1.6", only: [:test, :dev], runtime: false}, diff --git a/mix.lock b/mix.lock index d3d9b7b..0ef24ba 100644 --- a/mix.lock +++ b/mix.lock @@ -2,6 +2,7 @@ "blitz_credo_checks": {:hex, :blitz_credo_checks, "0.1.6", "63ba37bc202e44c7d0623720f6f74fe5b8ba1856b786c1365d07d818316f6f5d", [:mix], [{:credo, "~> 1.4", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "f9c602aea15f6cbe67cc9dfb9e786985f7ead8e78f19237b96d823373a380d82"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "con_cache": {:hex, :con_cache, "1.0.0", "6405e2bd5d5005334af72939432783562a8c35a196c2e63108fe10bb97b366e6", [:mix], [], "hexpm", "4d1f5cb1a67f3c1a468243dc98d10ac83af7f3e33b7e7c15999dc2c9bc0a551e"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"}, "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"}, @@ -10,7 +11,9 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"}, "ecto": {:hex, :ecto, "3.8.1", "35e0bd8c8eb772e14a5191a538cd079706ecb45164ea08a7523b4fc69ab70f56", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f1b68f8d5fe3ab89e24f57c03db5b5d0aed3602077972098b3a6006a1be4b69b"}, "ecto_sql": {:hex, :ecto_sql, "3.8.1", "1acaaba32ca0551fd19e492fc7c80414e72fc1a7140fc9395aaa53c2e8629798", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba7fc75882edce6f2ceca047315d5db27ead773cafea47f1724e35f1e7964525"}, + "elixir_cache": {:hex, :elixir_cache, "0.3.0", "2e11ecfc11c95389b8d912e0d4d277f4661a1355c9422d5c01162c3bf0b8f1bb", [:mix], [{:con_cache, "~> 1.0", [hex: :con_cache, repo: "hexpm", optional: false]}, {:error_message, "~> 0.3", [hex: :error_message, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 1.2", [hex: :redix, repo: "hexpm", optional: false]}, {:sandbox_registry, "~> 0.1", [hex: :sandbox_registry, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "524f9880b90ba2ecea12d13e695e9ba63d453d68ff5644caf79fa06e8f3a0e34"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "error_message": {:hex, :error_message, "0.3.1", "f8a3d90786e99d1af42400d9ab2e6d55543f604b08dd20999ff39b7712940876", [:mix], [{:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "96a8bce8bff8a410985ae5150809ebfcae659690ebea3d6792e25493797ea89c"}, "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, "excoveralls": {:hex, :excoveralls, "0.14.5", "5c685449596e962c779adc8f4fb0b4de3a5b291c6121097572a3aa5400c386d3", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9b4a9bf10e9a6e48b94159e13b4b8a1b05400f17ac16cc363ed8734f26e1f4e"}, "faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"}, @@ -22,12 +25,19 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"}, + "nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, + "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.16.3", "fac79a81a9a234b11c44235a4494d8565303fa4b9147acf57e48978a074971db", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "aeaae1d2d1322da4e5fe90d241b0a564ce03a3add09d7270fb85362166194590"}, + "redix": {:hex, :redix, "1.2.3", "3036e7c6080c42e1bbaa9168d1e28e367b01e8960a640a899b8ef8067273cb5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "14e2bca8a03fad297a78a3d201032df260ee5f0e0ef9c173c0f9ca5b3e0331b7"}, + "sandbox_registry": {:hex, :sandbox_registry, "0.1.1", "db6a116bf8e9553111820274701e48d1971185e89b53044f47c2a8f8e74956d7", [:mix], [], "hexpm", "88e12dc6be1bb98a05439e2b1de87db4bc0c043222b4fe4b46b5da7a0cc44948"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/test/test_helper.exs b/test/test_helper.exs index 1c32982..8ba2cb8 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,3 +1,17 @@ ExUnit.start() -{:ok, _} = FactoryEx.Support.Repo.start_link() +Application.put_env(:factory_ex, :ecto_repos, [FactoryEx.Support.Repo]) + +Application.put_env(:factory_ex, :sql_sandbox, true) + +{:ok, _} = FactoryEx.Support.Repo.start_link([ + username: "postgres", + password: "postgres", + database: "factory_ex_test", + hostname: "localhost", + pool: Ecto.Adapters.SQL.Sandbox, + pool_size: 10 +]) + +Cache.start_link([FactoryEx.FactoryCache]) +FactoryEx.FactoryCache.setup() From 6d3c4bec6c9c2f205feea6ea73c70aee2b3f2aa5 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Sat, 1 Jul 2023 20:38:01 -0700 Subject: [PATCH 06/26] add some documentation --- README.md | 57 ++++++++ lib/factory_ex.ex | 2 +- lib/factory_ex/association_builder.ex | 197 ++++++++++++-------------- lib/factory_ex/factory_cache.ex | 47 +----- test/test_helper.exs | 2 +- 5 files changed, 160 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index a9d2076..259b271 100644 --- a/README.md +++ b/README.md @@ -117,3 +117,60 @@ $ mix factory_ex.gen --repo FactoryEx.Support.Repo FactoryEx.Support.{Accounts.{ ``` To read more info run `mix factory_ex.gen` + +### Build Relational Associations + +FactoryEx can also build relational data structures based on your Ecto Schemas. This feature is +used to work with associations as a whole and aims to reduce test boilerplate. For example, +if a Team has many Users, it can create the parameters and/or associations automatically for you. +If your goal is to simply add a new user to a team, then it is preferred to do so manually. + +To create many associations you can specify a tuple of `{count, params}` which are expanded to a list +of params before building the factory. For example given a tuple of `{2, %{name: "John"}}` it will +expand to `[%{name: "John"}, %{name: "John"}]`. This can be added inside of lists or as values in +the map of parameters. You can also manually specify parameters per item when you want to create many +params and override specific values. For example given three items if you wanted to customize the name +for one you can do `[%{}, %{name: "custom"}, %{}]` or `[{2, %{}}, %{name: "custom"}]`. + +Let's take a look at an example: + +```elixir +user_jane_doe = FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.User, %{name: "Jane Doe"}) + +team = FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.Team) + +[random_user_one, random_user_two] = FactoryEx.insert_many!(2, FactoryEx.Support.Factory.Accounts.User, %{team_id: team.id}) + +FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.Label, %{user_id: random_user_one.id}) +FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.Label, %{user_id: random_user_two.id}) + +user_john_doe = FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.User, %{name: "John Doe", team_id: team.id}) +``` + +This can also be written as: + +```elixir +%{ + team: %{ + users: [user_john_doe, random_user_one, random_user_two] + } = team +} = user_jane_doe = + FactoryEx.insert!( + FactoryEx.Support.Factory.Accounts.User, + %{ + name: "Jane Doe", + team: %{ + users: [%{name: "John Doe"}, {2, %{}}] + } + }, + relational: [:role, team: [users: [:labels]]] + ) +``` + +Note: While this can simplify the way you write boilerplate it comes with a tradeoff as it groups +more of your data together which can hurt readability as well as make selecting specific pieces of +data harder. + +By default when building associations your params are put as associations on the changesets and will +be validated by your changeset validations. If this behavior is not desired you can set `validate` +to false and the params are deep converted to structs only. \ No newline at end of file diff --git a/lib/factory_ex.ex b/lib/factory_ex.ex index 1f4cd9e..a1f4c53 100644 --- a/lib/factory_ex.ex +++ b/lib/factory_ex.ex @@ -6,7 +6,7 @@ defmodule FactoryEx do "Sets the type of keys to have in the built object, can be one of `:atom`, `:string` or `:camel_string`" ], relational: [ - type: {:or, [{:list, :atom}, :keyword_list]}, + type: {:or, [{:list, :any}, :keyword_list]}, doc: "Sets the ecto schema association fields to generate, can be a list of `:atom` or `:keyword_list`" ], check_owner_key?: [ diff --git a/lib/factory_ex/association_builder.ex b/lib/factory_ex/association_builder.ex index ccc7b6f..7de3321 100644 --- a/lib/factory_ex/association_builder.ex +++ b/lib/factory_ex/association_builder.ex @@ -3,79 +3,88 @@ defmodule FactoryEx.AssociationBuilder do This module implements the api for auto generating Ecto Associations with factories. - To use this api you must pass keys (which can be type of `:list` or `:keyword_list`) to the - `relational` option. These keys are used by the `build_params/3` to find the appropriate - Factory for an Ecto Schema and invoke the `build_params/3` callback function. This allows you - to build any relational data structure declaratively. + ## Requirements - In the following section we will explain how `build_params/3` works with relational keys: + This module expects the `FactoryEx.FactoryCache` to have been started and initialized. + See the module documentation for more information. - - For each relational key, we try to fetch the the ecto schema's associations. If the field does - not exist for the given schema an error is raised otherwise the association is used to create - one or many params for the field based on the association's cardinality. + ## How to Use - - If the `owner_key` of the association's schema field is not set, the factory's `build/1` - function will be invoked with the field's existing value. If the owner key is set the - field is skipped and the existing value will be kept. Any existing parameters not passed - as a relational key will be kept. If this behaviour is not desired you can set the - `check_owner_field?` option to `false` and the parameters will be generated when the - owner key is set. + The `relational` option is used to auto-generate parameters based on the keys. Each relational field + must be a valid association. + + For example let's say you had a schema `Team` that had many `User` associations you can call build + params with the `Team` factory and pass in the `users` association field as a relational argument. + + ```elixir + FactoryEx.AssociationBuilder.build_params( + FactoryEx.Support.Factory.Accounts.Team, + %{}, + relational: [:users] + ) + ``` + + We don't specify `team` in the relational keys since the team is the root level schema and the + association builder expects all fields at the top level to be valid associations of the `Team` schema. + + Because the association builder traverses your Ecto.Schema's associations based on the fields if + the field specified does not exist as a valid association on the schema an error is raised. ## Examples - # Create params for a one-to-one relationship - FactoryEx.AssociationBuilder.build_params( - FactoryEx.Support.Factory.Accounts.User, - %{pre_existing_params: "hello world!"}, - relational: [:role, :team] - ) - %{ - pre_existing_params: "hello world!", - role: %{code: "Utah cats"}, - team: %{name: "Macejkovic Group"} - } - - # Create params for a one-to-many relationship - FactoryEx.AssociationBuilder.build_params( - FactoryEx.Support.Factory.Accounts.TeamOrganization, - %{teams: [%{}, %{}]}, - relational: [:teams] - ) - %{teams: [%{name: "Lindgren-Zemlak"}, %{name: "Kutch Group"}]} - - # Create deep relational structure and override specific field values - FactoryEx.AssociationBuilder.build_params( - FactoryEx.Support.Factory.Accounts.TeamOrganization, - %{teams: [%{name: "team name goes here", users: [%{name: "first user name"}, %{}]}]}, - relational: [teams: [users: [:labels, :role]]] - ) - %{ - teams: [ - %{ - name: "team name goes here", - users: [ - %{ - birthday: ~D[1992-10-04], - email: "ivy_lind@braun.info", - gender: "male", - labels: [%{label: "expedita"}], - location: "someplace", - name: "first user name", - role: %{code: "Iowa penguins"} - }, - %{ - birthday: ~D[1992-10-04], - email: "dillan1930@nolan.biz", - gender: "male", - labels: [%{label: "exercitationem"}], - location: "someplace", - name: "Name Zulauf Jr.", - role: %{code: "New Hampshire dwarves"} - } - ] - } - ] - } + ```elixir + # + # Create params for a one-to-one relationship + # + FactoryEx.AssociationBuilder.build_params( + FactoryEx.Support.Factory.Accounts.User, + %{name: "Jane Doe"}, + relational: [:role, :team] + ) + %{ + name: "Jane Doe", + role: %{code: "Utah cats"}, + team: %{name: "Macejkovic Group"} + } + + # + # Create a specific count params for a one-to-many relationship + # + FactoryEx.AssociationBuilder.build_params( + FactoryEx.Support.Factory.Accounts.TeamOrganization, + %{teams: [%{}, %{name: "awesome team name"}]}, + relational: [:teams] + ) + %{teams: [%{name: "Lindgren-Zemlak"}, %{name: "awesome team name"}]} + + # + # You can also build deep relational structures + # + FactoryEx.AssociationBuilder.build_params( + FactoryEx.Support.Factory.Accounts.TeamOrganization, + %{}, + relational: [teams: [users: [:labels, :role]]] + ) + %{ + teams: [ + %{ + name: "Leffler Group", + users: [ + %{ + birthday: ~D[1992-10-04], + email: "suzanne.yundt@armstrong.info", + gender: "male", + labels: [%{label: "autem"}], + location: "someplace", + name: "Gerda Waelchi", + role: %{code: "North Carolina whales"} + } + ] + } + ] + } + ``` + """ @doc """ @@ -85,70 +94,52 @@ defmodule FactoryEx.AssociationBuilder do def build_params(factory_module, params \\ %{}, options \\ []) do schema = factory_module.schema() assoc_fields = Keyword.get(options, :relational, []) - check_owner_key? = Keyword.get(options, :check_owner_key?, true) - convert_fields_to_params(schema, params, assoc_fields, check_owner_key?) + convert_fields_to_params(schema, params, assoc_fields) end - defp convert_fields_to_params(schema, params, assoc_fields, check_owner_key?) do - Enum.reduce(assoc_fields, params, &create_schema_params(schema, &1, &2, check_owner_key?)) + defp convert_fields_to_params(schema, params, assoc_fields) do + Enum.reduce(assoc_fields, params, &create_schema_params(schema, &1, &2)) end - defp create_schema_params(schema, {field, assoc_fields}, params, check_owner_key?) do + defp create_schema_params(schema, {field, assoc_fields}, params) do schema |> fetch_assoc!(field) - |> create_one_or_many_params(params, field, assoc_fields, check_owner_key?) + |> create_one_or_many_params(params, field, assoc_fields) |> case do nil -> params assoc_params -> Map.put(params, field, assoc_params) end end - defp create_schema_params(schema, field, params, check_owner_key?) do - create_schema_params(schema, {field, []}, params, check_owner_key?) + defp create_schema_params(schema, field, params) do + create_schema_params(schema, {field, []}, params) end defp create_one_or_many_params( - %{cardinality: :many, queryable: queryable} = assoc, + %{cardinality: :many, queryable: queryable}, params, field, - assoc_fields, - check_owner_key? + assoc_fields ) do - if check_owner_key? and owner_key_is_set?(assoc, params) do - Map.get(params, field) - else - params - |> Map.get(field, [%{}]) - |> Enum.map(&factory_build(queryable, &1, assoc_fields, check_owner_key?)) - end + params + |> Map.get(field, [%{}]) + |> Enum.map(&factory_build(queryable, &1, assoc_fields)) end defp create_one_or_many_params( - %{cardinality: :one, queryable: queryable} = assoc, + %{cardinality: :one, queryable: queryable}, params, field, - assoc_fields, - check_owner_key? + assoc_fields ) do - if check_owner_key? and owner_key_is_set?(assoc, params) do - Map.get(params, field) - else - params = Map.get(params, field, %{}) - factory_build(queryable, params, assoc_fields, check_owner_key?) - end - end - - defp owner_key_is_set?(assoc, params) do - case Map.get(params, assoc.owner_key) do - nil -> false - _ -> true - end + params = Map.get(params, field, %{}) + factory_build(queryable, params, assoc_fields) end - defp factory_build(queryable, params, assoc_fields, check_owner_key?) do + defp factory_build(queryable, params, assoc_fields) do parent = FactoryEx.FactoryCache.build_params(queryable, params) - assoc = convert_fields_to_params(queryable, params, assoc_fields, check_owner_key?) + assoc = convert_fields_to_params(queryable, params, assoc_fields) Map.merge(parent, assoc) end diff --git a/lib/factory_ex/factory_cache.ex b/lib/factory_ex/factory_cache.ex index 5c97b00..5f64b2b 100644 --- a/lib/factory_ex/factory_cache.ex +++ b/lib/factory_ex/factory_cache.ex @@ -2,7 +2,6 @@ defmodule FactoryEx.FactoryCache do @moduledoc """ KV store that maps Ecto.Schema modules keys to Factory module values. - # Getting Started To use this cache it must first be started. It can be added a child to your @@ -59,6 +58,8 @@ defmodule FactoryEx.FactoryCache do ## Requirements + The following steps are required to detect your factories: + - Your application must have `factory_ex` as a dependency in `mix.exs`. - Your application defines a factory module for each schema used as a relational key - Your module contains the prefix `Factory`, ie. YourApp.Support.Factory.Schemas.Schema. @@ -75,46 +76,14 @@ defmodule FactoryEx.FactoryCache do ```elixir config :factory_ex, :factory_module_prefix, Factory ``` - - ## Test Isolation - - The factories can be isolated per test by enabling the sandbox through the config at *compile* time: - - ```elixir - config :factory_ex, :sandbox_factory_cache?, true - ``` - - This method relies on `Cache.SandboxRegistry` which has to be started before your tests: - - ```elixir - # test_helper.exs - Cache.SandboxRegistry.start_link() - ``` - - Once the sandbox registry is started you must then register the cache and put a value for - the store for each test. This can be done in a setup block: - - ```elixir - # your test file - defmodule YourModuleTest do - setup do - Cache.SandboxRegistry.register_caches(FactoryEx.FactoryCache) - - FactoryEx.FactoryCache.put_store(%{ - FactoryEx.Support.Schema.Accounts.User => FactoryEx.Support.Factory.Accounts.User - }) - end - end - ``` """ @app :factory_ex - @sandbox_enabled Application.compile_env(@app, :sandbox_factory_cache?, false) @factory_prefix Application.compile_env(@app, :factory_module_prefix, Factory) use Cache, adapter: Cache.ETS, name: :factory_ex_factory_store, - sandbox?: @sandbox_enabled, + sandbox?: false, opts: [] @store :store @@ -139,14 +108,12 @@ defmodule FactoryEx.FactoryCache do def get_store() do with :ok <- ensure_cache_started!(), {:ok, nil} <- get(@store) do - raise """ - FactoryCache store not found! To fix this error call `FactoryEx.FactoryCache.setup/0`. - """ + raise "FactoryCache store not found! To fix this error call `FactoryEx.FactoryCache.setup/0`." end end defp ensure_cache_started! do - if @sandbox_enabled or cache_started?() do + if cache_started?() do :ok else raise """ @@ -169,13 +136,13 @@ defmodule FactoryEx.FactoryCache do end end - @doc "Builds the store and hydrates the cache." + @doc "Puts the result of `build_store/0` in the store." @spec setup :: :ok def setup do put_store(build_store()) end - @doc "Builds the store" + @doc "Collects factories and groups them by schema." @spec build_store :: map() def build_store do Enum.reduce( diff --git a/test/test_helper.exs b/test/test_helper.exs index 8ba2cb8..518b56b 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -4,7 +4,7 @@ Application.put_env(:factory_ex, :ecto_repos, [FactoryEx.Support.Repo]) Application.put_env(:factory_ex, :sql_sandbox, true) -{:ok, _} = FactoryEx.Support.Repo.start_link([ +FactoryEx.Support.Repo.start_link([ username: "postgres", password: "postgres", database: "factory_ex_test", From 62d8e1209d1713a39b2c6d22891f34311f6ca8d0 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Sat, 1 Jul 2023 20:43:41 -0700 Subject: [PATCH 07/26] make credo happy --- lib/factory_ex.ex | 16 ++++++---------- lib/factory_ex/factory_cache.ex | 4 ++-- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/factory_ex.ex b/lib/factory_ex.ex index a1f4c53..9796591 100644 --- a/lib/factory_ex.ex +++ b/lib/factory_ex.ex @@ -8,10 +8,6 @@ defmodule FactoryEx do relational: [ type: {:or, [{:list, :any}, :keyword_list]}, doc: "Sets the ecto schema association fields to generate, can be a list of `:atom` or `:keyword_list`" - ], - check_owner_key?: [ - type: :boolean, - doc: "Sets the behaviour for handling associated parameters when the owner key is set, can be `true` or `false`. Defaults to `true`." ] ] @@ -137,13 +133,13 @@ defmodule FactoryEx do def build(module, params, options) do Code.ensure_loaded(module.schema()) - validate? = Keyword.get(options, :validate, true) + validate = Keyword.get(options, :validate, true) params |> Utils.expand_count_tuples() |> module.build() |> then(&AssociationBuilder.build_params(module, &1, options)) - |> maybe_create_changeset(module, validate?) + |> maybe_create_changeset(module, validate) |> case do %Ecto.Changeset{} = changeset -> Ecto.Changeset.apply_action!(changeset, :insert) struct when is_struct(struct) -> struct @@ -164,13 +160,13 @@ defmodule FactoryEx do def insert!(module, params, options) do Code.ensure_loaded(module.schema()) - validate? = Keyword.get(options, :validate, true) + validate = Keyword.get(options, :validate, true) params |> Utils.expand_count_tuples() |> module.build() |> then(&AssociationBuilder.build_params(module, &1, options)) - |> maybe_create_changeset(module, validate?) + |> maybe_create_changeset(module, validate) |> module.repo().insert!(options) end @@ -193,8 +189,8 @@ defmodule FactoryEx do module.repo().delete_all(module.schema(), options) end - defp maybe_create_changeset(params, module, validate?) do - if validate? && schema?(module) do + defp maybe_create_changeset(params, module, validate) do + if validate && schema?(module) do params = Utils.deep_struct_to_map(params) if create_changeset_defined?(module.schema()) do diff --git a/lib/factory_ex/factory_cache.ex b/lib/factory_ex/factory_cache.ex index 5f64b2b..e9ebcae 100644 --- a/lib/factory_ex/factory_cache.ex +++ b/lib/factory_ex/factory_cache.ex @@ -104,8 +104,8 @@ defmodule FactoryEx.FactoryCache do end @doc "Returns the store." - @spec get_store() :: {:ok, map()} | {:ok, nil} - def get_store() do + @spec get_store :: {:ok, map()} | {:ok, nil} + def get_store do with :ok <- ensure_cache_started!(), {:ok, nil} <- get(@store) do raise "FactoryCache store not found! To fix this error call `FactoryEx.FactoryCache.setup/0`." From f52d10f886a444f6a935a6c2d0c20d9e7100eaa5 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Sat, 1 Jul 2023 20:52:54 -0700 Subject: [PATCH 08/26] cleanup --- lib/factory_ex/factory_cache.ex | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/lib/factory_ex/factory_cache.ex b/lib/factory_ex/factory_cache.ex index e9ebcae..6704c55 100644 --- a/lib/factory_ex/factory_cache.ex +++ b/lib/factory_ex/factory_cache.ex @@ -26,15 +26,14 @@ defmodule FactoryEx.FactoryCache do By default the schemas in factory_ex are shown. Here's an example: ```elixir - FactoryEx.FactoryCache.get_store() - {:ok, + FactoryEx.FactoryCache.fetch_store!() %{ FactoryEx.Support.Schema.Accounts.Label => FactoryEx.Support.Factory.Accounts.Label, FactoryEx.Support.Schema.Accounts.Role => FactoryEx.Support.Factory.Accounts.Role, FactoryEx.Support.Schema.Accounts.Team => FactoryEx.Support.Factory.Accounts.Team, FactoryEx.Support.Schema.Accounts.TeamOrganization => FactoryEx.Support.Factory.Accounts.TeamOrganization, FactoryEx.Support.Schema.Accounts.User => FactoryEx.Support.Factory.Accounts.User - }} + } ``` Once the store is created you can build parameters for a given schema with `FactoryEx.FactoryCache.build_params/2`. @@ -95,17 +94,13 @@ defmodule FactoryEx.FactoryCache do store end - @doc "Puts a value in the store." - @spec put_store(map()) :: {:ok, map()} | {:ok, nil} - def put_store(val) do + defp put_store(val) do with :ok <- ensure_cache_started!() do put(@store, val) end end - @doc "Returns the store." - @spec get_store :: {:ok, map()} | {:ok, nil} - def get_store do + defp get_store do with :ok <- ensure_cache_started!(), {:ok, nil} <- get(@store) do raise "FactoryCache store not found! To fix this error call `FactoryEx.FactoryCache.setup/0`." @@ -139,17 +134,9 @@ defmodule FactoryEx.FactoryCache do @doc "Puts the result of `build_store/0` in the store." @spec setup :: :ok def setup do - put_store(build_store()) - end - - @doc "Collects factories and groups them by schema." - @spec build_store :: map() - def build_store do - Enum.reduce( - [@app | FactoryEx.Utils.apps_that_depend_on(@app)], - %{}, - &lookup_factory_modules/2 - ) + [@app | FactoryEx.Utils.apps_that_depend_on(@app)] + |> Enum.reduce(%{}, &lookup_factory_modules/2) + |> put_store() end defp lookup_factory_modules(app, acc) do From ae19c96d83bf015979a4e7f19522f8caf7daf044 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Sat, 1 Jul 2023 20:54:23 -0700 Subject: [PATCH 09/26] make more readable --- lib/factory_ex/factory_cache.ex | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/factory_ex/factory_cache.ex b/lib/factory_ex/factory_cache.ex index 6704c55..8f13ff1 100644 --- a/lib/factory_ex/factory_cache.ex +++ b/lib/factory_ex/factory_cache.ex @@ -76,15 +76,14 @@ defmodule FactoryEx.FactoryCache do config :factory_ex, :factory_module_prefix, Factory ``` """ - @app :factory_ex - @factory_prefix Application.compile_env(@app, :factory_module_prefix, Factory) - use Cache, adapter: Cache.ETS, name: :factory_ex_factory_store, sandbox?: false, opts: [] + @app :factory_ex + @factory_prefix Application.compile_env(@app, :factory_module_prefix, Factory) @store :store @doc "Returns the store map. Raises if no values exist." @@ -94,6 +93,14 @@ defmodule FactoryEx.FactoryCache do store end + @doc "Puts the result of `build_store/0` in the store." + @spec setup :: :ok + def setup do + [@app | FactoryEx.Utils.apps_that_depend_on(@app)] + |> Enum.reduce(%{}, &lookup_factory_modules/2) + |> put_store() + end + defp put_store(val) do with :ok <- ensure_cache_started!() do put(@store, val) @@ -131,14 +138,6 @@ defmodule FactoryEx.FactoryCache do end end - @doc "Puts the result of `build_store/0` in the store." - @spec setup :: :ok - def setup do - [@app | FactoryEx.Utils.apps_that_depend_on(@app)] - |> Enum.reduce(%{}, &lookup_factory_modules/2) - |> put_store() - end - defp lookup_factory_modules(app, acc) do app |> FactoryEx.Utils.find_app_modules(@factory_prefix) From 1e9c66738cab9842f5f5330a0531e7f42647ba36 Mon Sep 17 00:00:00 2001 From: Nick Acker Date: Wed, 5 Apr 2023 10:55:43 -0700 Subject: [PATCH 10/26] Update nimble_options to 1.0 (#9) --- mix.exs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 47c5e88..7cbf802 100644 --- a/mix.exs +++ b/mix.exs @@ -42,11 +42,13 @@ defmodule FactoryEx.MixProject do defp deps do [ {:ecto, "~> 3.0"}, + {:ecto_sql, "~> 3.0", only: [:test, :dev], optional: true}, + {:postgrex, "~> 0.16", only: [:test, :dev], optional: true}, + {:faker, ">= 0.0.0"}, {:nimble_options, "~> 0.5"}, {:elixir_cache, "~> 0.3.0"}, - {:ecto_sql, "~> 3.0", only: [:test, :dev], optional: true}, - {:postgrex, "~> 0.16", only: [:test, :dev], optional: true}, + {:credo, "~> 1.6", only: [:test, :dev], runtime: false}, {:blitz_credo_checks, "~> 0.1", only: [:test, :dev], runtime: false}, {:excoveralls, "~> 0.10", only: :test}, From ae512a4f7ba8b2ecee3093b214e074ec9d6e62de Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Mon, 17 Jul 2023 11:15:52 -0700 Subject: [PATCH 11/26] unpin elixir_cache version --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 0ef24ba..03fb598 100644 --- a/mix.lock +++ b/mix.lock @@ -11,7 +11,7 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"}, "ecto": {:hex, :ecto, "3.8.1", "35e0bd8c8eb772e14a5191a538cd079706ecb45164ea08a7523b4fc69ab70f56", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f1b68f8d5fe3ab89e24f57c03db5b5d0aed3602077972098b3a6006a1be4b69b"}, "ecto_sql": {:hex, :ecto_sql, "3.8.1", "1acaaba32ca0551fd19e492fc7c80414e72fc1a7140fc9395aaa53c2e8629798", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba7fc75882edce6f2ceca047315d5db27ead773cafea47f1724e35f1e7964525"}, - "elixir_cache": {:hex, :elixir_cache, "0.3.0", "2e11ecfc11c95389b8d912e0d4d277f4661a1355c9422d5c01162c3bf0b8f1bb", [:mix], [{:con_cache, "~> 1.0", [hex: :con_cache, repo: "hexpm", optional: false]}, {:error_message, "~> 0.3", [hex: :error_message, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 1.2", [hex: :redix, repo: "hexpm", optional: false]}, {:sandbox_registry, "~> 0.1", [hex: :sandbox_registry, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "524f9880b90ba2ecea12d13e695e9ba63d453d68ff5644caf79fa06e8f3a0e34"}, + "elixir_cache": {:hex, :elixir_cache, "0.3.1", "da55516e4a0760760353d9e343caf8582dac18ca6d664db683aecc9a2425db60", [:mix], [{:con_cache, "~> 1.0", [hex: :con_cache, repo: "hexpm", optional: false]}, {:error_message, "~> 0.3", [hex: :error_message, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 1.2", [hex: :redix, repo: "hexpm", optional: false]}, {:sandbox_registry, "~> 0.1", [hex: :sandbox_registry, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5a4f96af7c760c0ab75ba7272b2ff24e573053c438e361033c45047d4ccb48ca"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "error_message": {:hex, :error_message, "0.3.1", "f8a3d90786e99d1af42400d9ab2e6d55543f604b08dd20999ff39b7712940876", [:mix], [{:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "96a8bce8bff8a410985ae5150809ebfcae659690ebea3d6792e25493797ea89c"}, "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, From bc2e35083d1b45f885e967aa8f43b4e7f5f1119f Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Wed, 26 Jul 2023 12:51:09 -0700 Subject: [PATCH 12/26] chore: improve error message --- lib/factory_ex/factory_cache.ex | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/factory_ex/factory_cache.ex b/lib/factory_ex/factory_cache.ex index 8f13ff1..cd1869a 100644 --- a/lib/factory_ex/factory_cache.ex +++ b/lib/factory_ex/factory_cache.ex @@ -78,7 +78,7 @@ defmodule FactoryEx.FactoryCache do """ use Cache, adapter: Cache.ETS, - name: :factory_ex_factory_store, + name: :factory_ex_factory_cache, sandbox?: false, opts: [] @@ -93,7 +93,7 @@ defmodule FactoryEx.FactoryCache do store end - @doc "Puts the result of `build_store/0` in the store." + @doc "Creates the key-value store." @spec setup :: :ok def setup do [@app | FactoryEx.Utils.apps_that_depend_on(@app)] @@ -110,7 +110,20 @@ defmodule FactoryEx.FactoryCache do defp get_store do with :ok <- ensure_cache_started!(), {:ok, nil} <- get(@store) do - raise "FactoryCache store not found! To fix this error call `FactoryEx.FactoryCache.setup/0`." + raise """ + Factories not found! + + Add the following to your test_helper.exs: + + ``` + # test_helper.exs + FactoryEx.FactoryCache.setup() + ``` + + If setup if already called in test_helper.exs ensure factory_ex is a dependency of + all applications that contain factories you want to load and they have the module + prefix #{inspect(@factory_prefix)}. + """ end end From ed05dd356240bc95ccc3ff2125194d99f9c7633a Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Mon, 7 Aug 2023 16:18:37 -0400 Subject: [PATCH 13/26] fix factory cache in umbrella app --- .tool-versions | 2 + lib/factory_ex/factory_cache.ex | 111 +++++++------------------------- 2 files changed, 27 insertions(+), 86 deletions(-) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..2a6a644 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.14.2 +erlang 25.1.2 \ No newline at end of file diff --git a/lib/factory_ex/factory_cache.ex b/lib/factory_ex/factory_cache.ex index cd1869a..786acee 100644 --- a/lib/factory_ex/factory_cache.ex +++ b/lib/factory_ex/factory_cache.ex @@ -12,49 +12,15 @@ defmodule FactoryEx.FactoryCache do Cache.start_link([FactoryEx.FactoryCache]) ``` - Once the cache is started the store then needs to be created. + Once the cache add the following setup before your tests: ```elixir - FactoryEx.FactoryCache.setup() - :ok - ``` - - This function finds your factory modules, maps them to their schemas and stores - the result. This also works with umbrella apps out of the box. - - We can now view the modules in the store with `FactoryEx.FactoryCache.get_store/0`. - By default the schemas in factory_ex are shown. Here's an example: - - ```elixir - FactoryEx.FactoryCache.fetch_store!() - %{ - FactoryEx.Support.Schema.Accounts.Label => FactoryEx.Support.Factory.Accounts.Label, - FactoryEx.Support.Schema.Accounts.Role => FactoryEx.Support.Factory.Accounts.Role, - FactoryEx.Support.Schema.Accounts.Team => FactoryEx.Support.Factory.Accounts.Team, - FactoryEx.Support.Schema.Accounts.TeamOrganization => FactoryEx.Support.Factory.Accounts.TeamOrganization, - FactoryEx.Support.Schema.Accounts.User => FactoryEx.Support.Factory.Accounts.User - } - ``` - - Once the store is created you can build parameters for a given schema with `FactoryEx.FactoryCache.build_params/2`. - Let's go through an example using a schema in the store: - - ```elixir - FactoryEx.FactoryCache.build_params(FactoryEx.Support.Schema.Accounts.User, %{location: "custom_location"}) - %{ - birthday: ~D[1992-10-04], - email: "tyrese_welch@beier.org", - gender: "male", - location: "custom_location", - name: "Elisa Abbott" - } + setup do + Cache.SandboxRegistry.register_caches(FactoryEx.FactoryCache) + FactoryEx.FactoryCache.setup() + end ``` - In the example above we called `FactoryEx.FactoryCache.build_params/2` with the schema - `FactoryEx.Support.Factory.Accounts.User` and some parameters. In the store the schema - key has the value of `FactoryEx.Support.Factory.Accounts.User`. This in turn calls - the factory's callback function `build/1` called with the parameters. - ## Requirements The following steps are required to detect your factories: @@ -79,18 +45,19 @@ defmodule FactoryEx.FactoryCache do use Cache, adapter: Cache.ETS, name: :factory_ex_factory_cache, - sandbox?: false, + sandbox?: true, opts: [] @app :factory_ex @factory_prefix Application.compile_env(@app, :factory_module_prefix, Factory) @store :store - @doc "Returns the store map. Raises if no values exist." - @spec fetch_store!() :: map() - def fetch_store! do - {:ok, store} = get_store() - store + @doc """ + Returns the result of the `build/1` function of the Factory associated with the given schema. + """ + @spec build_params(module, map) :: map + def build_params(ecto_schema, params \\ %{}) do + fetch_schema_factory!(ecto_schema).build(params) end @doc "Creates the key-value store." @@ -102,53 +69,33 @@ defmodule FactoryEx.FactoryCache do end defp put_store(val) do - with :ok <- ensure_cache_started!() do - put(@store, val) - end + put(@store, val) end defp get_store do - with :ok <- ensure_cache_started!(), - {:ok, nil} <- get(@store) do + with {:ok, nil} <- get(@store) do raise """ Factories not found! - Add the following to your test_helper.exs: + Add the following to your setup: ``` - # test_helper.exs - FactoryEx.FactoryCache.setup() + setup do + FactoryEx.FactoryCache.setup() + end ``` - If setup if already called in test_helper.exs ensure factory_ex is a dependency of - all applications that contain factories you want to load and they have the module - prefix #{inspect(@factory_prefix)}. - """ - end - end - - defp ensure_cache_started! do - if cache_started?() do - :ok - else - raise """ - FactoryEx.FactoryCache not started! - - Add the following to your test_helper.exs: + If setup/0 is already called ensure your application meets the requirements. + See the module documentation for FactoryEx.FactoryCache for more information + or `h FactoryEx.FactoryCache` in iex. - ``` - # test_helper.exs - Cache.start_link([FactoryEx.FactoryCache]) - ``` - """ + """ end end - defp cache_started? do - case :ets.whereis(FactoryEx.FactoryCache.cache_name()) do - :undefined -> false - _ -> true - end + defp fetch_store! do + {:ok, store} = get_store() + store end defp lookup_factory_modules(app, acc) do @@ -165,14 +112,6 @@ defmodule FactoryEx.FactoryCache do end end - @doc """ - Returns the result of the `build/1` function of the Factory associated with the given schema. - """ - @spec build_params(module, map) :: map - def build_params(ecto_schema, params \\ %{}) do - fetch_schema_factory!(ecto_schema).build(params) - end - defp fetch_schema_factory!(ecto_schema) do case Enum.find(fetch_store!(), fn {k, _} -> k === ecto_schema end) do nil -> From f53c3adb236c3f2b55e22bb8a8dff6df3b07ef39 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Mon, 30 Oct 2023 15:58:37 -0700 Subject: [PATCH 14/26] add already_started function --- lib/factory_ex/factory_cache.ex | 136 ++++++++++++++++++-------------- lib/factory_ex/utils.ex | 26 +++++- lib/mix/factory_ex_helpers.ex | 1 - test/support/datacase.ex | 2 + 4 files changed, 104 insertions(+), 61 deletions(-) diff --git a/lib/factory_ex/factory_cache.ex b/lib/factory_ex/factory_cache.ex index 786acee..e22b2b6 100644 --- a/lib/factory_ex/factory_cache.ex +++ b/lib/factory_ex/factory_cache.ex @@ -1,22 +1,37 @@ defmodule FactoryEx.FactoryCache do @moduledoc """ - KV store that maps Ecto.Schema modules keys to Factory module values. + This module implements a key-value store that maps Ecto.Schema modules + to Factory module values. This is used to automatically generate + relational ecto data structures through the `relational` option. # Getting Started - To use this cache it must first be started. It can be added a child to your - application supervisor or add the following snippet below to your test_helper.exs. + To use this cache you must first start it with `Cache.start_link/1`. + This can be added to your test_helper.exs file, for example: ```elixir # test_helper.exs Cache.start_link([FactoryEx.FactoryCache]) ``` + Once the cache is started you must call `FactoryEx.FactoryCache.setup/0` + before using the relational option in your tests. + Once the cache add the following setup before your tests: ```elixir - setup do - Cache.SandboxRegistry.register_caches(FactoryEx.FactoryCache) + setup_all do + FactoryEx.FactoryCache.setup() + end + ``` + + In umbrella apps you can also start the cache once by checking + if the cache has already started in your `test_helper.exs` file: + + ```elixir + # test_helper.exs + if !FactoryEx.FactoryCache.already_started?() do + Cache.start_link([FactoryEx.FactoryCache]) FactoryEx.FactoryCache.setup() end ``` @@ -30,13 +45,13 @@ defmodule FactoryEx.FactoryCache do - Your module contains the prefix `Factory`, ie. YourApp.Support.Factory.Schemas.Schema. - Your factory module defines the factory_ex `schema/0` callback function. - Note: In umbrella applications the requirements are per application. + In umbrella applications these requirements are per application. ## Factory Module Prefix If your application's factory modules do not use the prefix `Factory` or you want to change which factory modules are loaded during tests you can configure the module prefix option at - *compile* time with the following config: + compile time with the following config: ```elixir config :factory_ex, :factory_module_prefix, Factory @@ -45,60 +60,41 @@ defmodule FactoryEx.FactoryCache do use Cache, adapter: Cache.ETS, name: :factory_ex_factory_cache, - sandbox?: true, + sandbox?: false, opts: [] @app :factory_ex @factory_prefix Application.compile_env(@app, :factory_module_prefix, Factory) @store :store + @doc """ + Returns true if the cache is already started + """ + @spec already_started?() :: true | false + def already_started?, do: cache_name() |> :ets.whereis() |> is_reference() + @doc """ Returns the result of the `build/1` function of the Factory associated with the given schema. """ @spec build_params(module, map) :: map - def build_params(ecto_schema, params \\ %{}) do - fetch_schema_factory!(ecto_schema).build(params) - end + def build_params(ecto_schema, params \\ %{}), do: fetch_factory!(ecto_schema).build(params) - @doc "Creates the key-value store." + @doc """ + Creates the key-value store that maps Ecto.Schema modules to + their factory modules. + + This function aggregates all factory modules from every app that + depends on `factory_ex` and adds them to the store if they define + the `schema/0` callback function. + """ @spec setup :: :ok def setup do [@app | FactoryEx.Utils.apps_that_depend_on(@app)] - |> Enum.reduce(%{}, &lookup_factory_modules/2) + |> Enum.reduce(%{}, &aggregate_factory_modules/2) |> put_store() end - defp put_store(val) do - put(@store, val) - end - - defp get_store do - with {:ok, nil} <- get(@store) do - raise """ - Factories not found! - - Add the following to your setup: - - ``` - setup do - FactoryEx.FactoryCache.setup() - end - ``` - - If setup/0 is already called ensure your application meets the requirements. - See the module documentation for FactoryEx.FactoryCache for more information - or `h FactoryEx.FactoryCache` in iex. - - """ - end - end - - defp fetch_store! do - {:ok, store} = get_store() - store - end - - defp lookup_factory_modules(app, acc) do + defp aggregate_factory_modules(app, acc) do app |> FactoryEx.Utils.find_app_modules(@factory_prefix) |> Enum.reduce(acc, &maybe_put_factory_module/2) @@ -112,27 +108,51 @@ defmodule FactoryEx.FactoryCache do end end - defp fetch_schema_factory!(ecto_schema) do - case Enum.find(fetch_store!(), fn {k, _} -> k === ecto_schema end) do - nil -> - raise """ - Factory not found for schema '#{inspect(ecto_schema)}'. + defp fetch_factory!(ecto_schema) do + with nil <- Map.get(fetch_store!(), ecto_schema) do + raise """ + Factory not found for schema '#{inspect(ecto_schema)}'. + + This means the factory module does not exist or it was not loaded. + + To fix this error: + + - Add `factory_ex` as a depedency in `mix.exs` of the application that contains + the schema '#{inspect(ecto_schema)}'. In umbrella applications you must add + `factory_ex` as a dependency to each application that contain factory modules. + + - Create a factory for the schema '#{inspect(ecto_schema)}' + + - Add the prefix '#{inspect(@factory_prefix)}' to the factory module name. For example: + YourApp.#{inspect(@factory_prefix)}.Module. + """ + end + end - This means the factory module does not exist or it was not loaded. + defp put_store(state), do: put(@store, state) - To fix this error: + defp fetch_store! do + case get(@store) do + {:ok, nil} -> + raise """ + Factories not found! - - Add `factory_ex` as a depedency in `mix.exs` of the application that contains - the schema '#{inspect(ecto_schema)}'. In umbrella applications you must add - `factory_ex` as a dependency to each application that contain factory modules. + Add the following to your setup: - - Create a factory for the schema '#{inspect(ecto_schema)}' + ``` + setup do + FactoryEx.FactoryCache.setup() + end + ``` + + If setup/0 is already called ensure your application meets the requirements. + See the module documentation for FactoryEx.FactoryCache for more information + or `h FactoryEx.FactoryCache` in iex. - - Add the prefix '#{inspect(@factory_prefix)}' to the factory module name. For example: - YourApp.#{inspect(@factory_prefix)}.Module. """ - {_, factory} -> factory + {:ok, state} -> + state end end end diff --git a/lib/factory_ex/utils.ex b/lib/factory_ex/utils.ex index 8b3057e..8e70ae8 100644 --- a/lib/factory_ex/utils.ex +++ b/lib/factory_ex/utils.ex @@ -143,19 +143,41 @@ defmodule FactoryEx.Utils do end end + @doc """ + Returns all apps that have a dependency. + + ## Examples + + iex> FactoryEx.Utils.apps_that_depend_on(:ecto) + [:factory_ex, :ecto_sql] + """ def apps_that_depend_on(dep) do :application.loaded_applications() |> Enum.reduce([], fn {app, _, _}, acc -> deps = Application.spec(app)[:applications] - (dep in deps && acc ++ [app]) || acc + if dep in deps, do: acc ++ [app], else: acc end) end + @doc """ + Returns all application modules that start with a specific prefix. + + ## Examples + + iex> FactoryEx.Utils.find_app_modules(:factory_ex, Factory) + [ + FactoryEx.Support.Factory.Accounts.Label, + FactoryEx.Support.Factory.Accounts.Role, + FactoryEx.Support.Factory.Accounts.Team, + FactoryEx.Support.Factory.Accounts.TeamOrganization, + FactoryEx.Support.Factory.Accounts.User + ] + """ def find_app_modules(app, prefix) do case :application.get_key(app, :modules) do {:ok, modules} -> prefix = Module.split(prefix) - Enum.filter(modules, &(&1 |> Module.split() |> FactoryEx.Utils.sublist?(prefix))) + Enum.filter(modules, &(&1 |> Module.split() |> sublist?(prefix))) _ -> raise "modules not found for app #{inspect(app)}." end diff --git a/lib/mix/factory_ex_helpers.ex b/lib/mix/factory_ex_helpers.ex index 82a67e5..005ced9 100644 --- a/lib/mix/factory_ex_helpers.ex +++ b/lib/mix/factory_ex_helpers.ex @@ -20,7 +20,6 @@ defmodule Mix.FactoryExHelpers do ])), __STACKTRACE__ end - def schema_primary_key(ecto_schema) do ecto_schema.__schema__(:primary_key) end diff --git a/test/support/datacase.ex b/test/support/datacase.ex index f98930d..f7bea8d 100644 --- a/test/support/datacase.ex +++ b/test/support/datacase.ex @@ -16,6 +16,8 @@ defmodule FactoryEx.DataCase do use ExUnit.CaseTemplate + alias Ecto.Adapters.SQL.Sandbox + using do quote do alias FactoryEx.Support.Repo From f2ff4da2cf07c503b6b6dc5eebf1e3a953e745ca Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:03:56 -0700 Subject: [PATCH 15/26] fix conflicts --- mix.exs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/mix.exs b/mix.exs index 7cbf802..87ba0a3 100644 --- a/mix.exs +++ b/mix.exs @@ -6,8 +6,7 @@ defmodule FactoryEx.MixProject do app: :factory_ex, version: "0.3.3", elixir: "~> 1.13", - description: - "Factories for elixir to help create data models at random, this works for any type of ecto structs", + description: "Factories for elixir to help create data models at random, this works for any type of ecto structs", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps(), @@ -73,10 +72,8 @@ defmodule FactoryEx.MixProject do groups_for_modules: [ General: [ FactoryEx, - FactoryEx.SchemaCounter - ], - Adapters: [ - FactoryEx.Adapter + FactoryEx.SchemaCounter, + FactoryEx.FactoryCache ] ] ] From 4b8b38c2ce0f0ace3c8e5417d2f8f4d177c894cb Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:05:07 -0700 Subject: [PATCH 16/26] bump nimbleoptions to 0.5 --- mix.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mix.lock b/mix.lock index 03fb598..7269128 100644 --- a/mix.lock +++ b/mix.lock @@ -13,7 +13,7 @@ "ecto_sql": {:hex, :ecto_sql, "3.8.1", "1acaaba32ca0551fd19e492fc7c80414e72fc1a7140fc9395aaa53c2e8629798", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba7fc75882edce6f2ceca047315d5db27ead773cafea47f1724e35f1e7964525"}, "elixir_cache": {:hex, :elixir_cache, "0.3.1", "da55516e4a0760760353d9e343caf8582dac18ca6d664db683aecc9a2425db60", [:mix], [{:con_cache, "~> 1.0", [hex: :con_cache, repo: "hexpm", optional: false]}, {:error_message, "~> 0.3", [hex: :error_message, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 1.2", [hex: :redix, repo: "hexpm", optional: false]}, {:sandbox_registry, "~> 0.1", [hex: :sandbox_registry, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5a4f96af7c760c0ab75ba7272b2ff24e573053c438e361033c45047d4ccb48ca"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "error_message": {:hex, :error_message, "0.3.1", "f8a3d90786e99d1af42400d9ab2e6d55543f604b08dd20999ff39b7712940876", [:mix], [{:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "96a8bce8bff8a410985ae5150809ebfcae659690ebea3d6792e25493797ea89c"}, + "error_message": {:hex, :error_message, "0.3.2", "01fe015ba807b515ad1d9fcfcbeb49c399374393ef3fecf9de148a471198b300", [:mix], [{:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "b3c31bf0c618f7d5b812f581fdedff5829c89439134c724aab26aa958b5ad8fa"}, "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, "excoveralls": {:hex, :excoveralls, "0.14.5", "5c685449596e962c779adc8f4fb0b4de3a5b291c6121097572a3aa5400c386d3", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9b4a9bf10e9a6e48b94159e13b4b8a1b05400f17ac16cc363ed8734f26e1f4e"}, "faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"}, @@ -30,11 +30,11 @@ "nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, - "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, + "plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"}, + "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.16.3", "fac79a81a9a234b11c44235a4494d8565303fa4b9147acf57e48978a074971db", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "aeaae1d2d1322da4e5fe90d241b0a564ce03a3add09d7270fb85362166194590"}, - "redix": {:hex, :redix, "1.2.3", "3036e7c6080c42e1bbaa9168d1e28e367b01e8960a640a899b8ef8067273cb5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "14e2bca8a03fad297a78a3d201032df260ee5f0e0ef9c173c0f9ca5b3e0331b7"}, + "redix": {:hex, :redix, "1.3.0", "f4121163ff9d73bf72157539ff23b13e38422284520bb58c05e014b19d6f0577", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "60d483d320c77329c8cbd3df73007e51b23f3fae75b7693bc31120d83ab26131"}, "sandbox_registry": {:hex, :sandbox_registry, "0.1.1", "db6a116bf8e9553111820274701e48d1971185e89b53044f47c2a8f8e74956d7", [:mix], [], "hexpm", "88e12dc6be1bb98a05439e2b1de87db4bc0c043222b4fe4b46b5da7a0cc44948"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, From be4a96f656683e98e4113b8a8e9b60e2ba402fd4 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:50:39 -0700 Subject: [PATCH 17/26] update docs and improve error handling --- README.md | 89 ++++++++++--------- lib/factory_ex/association_builder.ex | 49 +++++----- lib/factory_ex/factory_cache.ex | 74 ++++++++------- mix.exs | 1 + test/support/datacase.ex | 18 +--- test/support/factory/accounts/label.ex | 4 +- test/support/factory/accounts/role.ex | 4 +- test/support/factory/accounts/team.ex | 4 +- .../factory/accounts/team_organization.ex | 4 +- test/support/factory/accounts/user.ex | 4 +- 10 files changed, 118 insertions(+), 133 deletions(-) diff --git a/README.md b/README.md index 259b271..0ee6e69 100644 --- a/README.md +++ b/README.md @@ -120,57 +120,66 @@ To read more info run `mix factory_ex.gen` ### Build Relational Associations -FactoryEx can also build relational data structures based on your Ecto Schemas. This feature is -used to work with associations as a whole and aims to reduce test boilerplate. For example, -if a Team has many Users, it can create the parameters and/or associations automatically for you. -If your goal is to simply add a new user to a team, then it is preferred to do so manually. +FactoryEx makes it possible to create associated records with your factories. This is +similar to creating to creating records with Ecto.Changeset `cast_assoc` or `put_assoc` +with the addition of using your factory to generate the parameters. To use this feature +you must pass the `relational` option. The `relational` option accepts a list of keys +that map to associations, for example if a Team has many users you would pass +`[relational: [:users]]`: -To create many associations you can specify a tuple of `{count, params}` which are expanded to a list -of params before building the factory. For example given a tuple of `{2, %{name: "John"}}` it will -expand to `[%{name: "John"}, %{name: "John"}]`. This can be added inside of lists or as values in -the map of parameters. You can also manually specify parameters per item when you want to create many -params and override specific values. For example given three items if you wanted to customize the name -for one you can do `[%{}, %{name: "custom"}, %{}]` or `[{2, %{}}, %{name: "custom"}]`. +```elixir + FactoryEx.AssociationBuilder.build_params( + FactoryEx.Support.Factory.Accounts.Team, + %{}, + relational: [:users] + ) +``` -Let's take a look at an example: +The goal of this feature is to reduce boilerplate code in your test. In this example we +create a team with 3 users that each have a label and a role: ```elixir -user_jane_doe = FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.User, %{name: "Jane Doe"}) +setup do + team = FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.Team) -team = FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.Team) + user_one = FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.User, %{team_id: team.id}) + FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.Label, %{user_id: user_one.id}) + FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.Role, %{user_id: user_one.id}) -[random_user_one, random_user_two] = FactoryEx.insert_many!(2, FactoryEx.Support.Factory.Accounts.User, %{team_id: team.id}) + user_two = FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.User, %{team_id: team.id}) + FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.Label, %{user_id: user_two.id}) + FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.Role, %{user_id: user_two.id}) -FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.Label, %{user_id: random_user_one.id}) -FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.Label, %{user_id: random_user_two.id}) - -user_john_doe = FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.User, %{name: "John Doe", team_id: team.id}) + user_three = FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.User, %{team_id: team.id}) + FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.Label, %{user_id: user_three.id}) + FactoryEx.insert!(FactoryEx.Support.Factory.Accounts.Role, %{user_id: user_three.id}) +end ``` -This can also be written as: +With the relational feature this can be written as: ```elixir -%{ - team: %{ - users: [user_john_doe, random_user_one, random_user_two] - } = team -} = user_jane_doe = - FactoryEx.insert!( - FactoryEx.Support.Factory.Accounts.User, - %{ - name: "Jane Doe", - team: %{ - users: [%{name: "John Doe"}, {2, %{}}] - } - }, - relational: [:role, team: [users: [:labels]]] - ) +setup do + team = + FactoryEx.insert!( + FactoryEx.Support.Factory.Accounts.Team, + %{users: {3, %{}}}, + relational: [users: [:labels, :role]] + ) +end ``` -Note: While this can simplify the way you write boilerplate it comes with a tradeoff as it groups -more of your data together which can hurt readability as well as make selecting specific pieces of -data harder. +You can create many associations by specifying a tuple of `{count, params}` which is expanded +to a list of params before building the params with a factory. For example if you pass a +tuple of `{2, %{name: "John"}}` it will be expanded to `[%{name: "John"}, %{name: "John"}]`. +The count tuples can be added as elements inside a list or as values in the map of +parameters. You can also manually add parameters which is useful for setting specific values +while creating a specific amount. For example given three items if you wanted to customize +the name for one you can do `[%{}, %{name: "custom"}, %{}]` or `[{2, %{}}, %{name: "custom"}]`. -By default when building associations your params are put as associations on the changesets and will -be validated by your changeset validations. If this behavior is not desired you can set `validate` -to false and the params are deep converted to structs only. \ No newline at end of file +By default parameters are validated by Ecto.Changeset. If this behavior is not desired you can +set the `validate` option to false which converts params to structs only. + +While this can simplify the amount of boilerplate you have to write it comes with a trade off +of creating large complex objects that can hurt readability and/or make accessing specific +data harder. diff --git a/lib/factory_ex/association_builder.ex b/lib/factory_ex/association_builder.ex index 7de3321..62da673 100644 --- a/lib/factory_ex/association_builder.ex +++ b/lib/factory_ex/association_builder.ex @@ -8,13 +8,13 @@ defmodule FactoryEx.AssociationBuilder do This module expects the `FactoryEx.FactoryCache` to have been started and initialized. See the module documentation for more information. - ## How to Use + ## Relational Builder - The `relational` option is used to auto-generate parameters based on the keys. Each relational field - must be a valid association. + This is an introduction to the `relational` option which is used to auto-generate + factory parameters based on the keys. Let's look at an example. - For example let's say you had a schema `Team` that had many `User` associations you can call build - params with the `Team` factory and pass in the `users` association field as a relational argument. + If a `Team` has many `User` associations you can build many params using the `Team` + factory and pass `users` as the field in the relational option. ```elixir FactoryEx.AssociationBuilder.build_params( @@ -24,18 +24,14 @@ defmodule FactoryEx.AssociationBuilder do ) ``` - We don't specify `team` in the relational keys since the team is the root level schema and the - association builder expects all fields at the top level to be valid associations of the `Team` schema. - - Because the association builder traverses your Ecto.Schema's associations based on the fields if - the field specified does not exist as a valid association on the schema an error is raised. + Note that we did not add the field `team` since the team is the parent schema and all fields + must be valid associations of the team schema. The association builder may raise if a field + is not a valid ecto association for the given factory's schema. ## Examples ```elixir - # # Create params for a one-to-one relationship - # FactoryEx.AssociationBuilder.build_params( FactoryEx.Support.Factory.Accounts.User, %{name: "Jane Doe"}, @@ -47,9 +43,7 @@ defmodule FactoryEx.AssociationBuilder do team: %{name: "Macejkovic Group"} } - # # Create a specific count params for a one-to-many relationship - # FactoryEx.AssociationBuilder.build_params( FactoryEx.Support.Factory.Accounts.TeamOrganization, %{teams: [%{}, %{name: "awesome team name"}]}, @@ -57,9 +51,7 @@ defmodule FactoryEx.AssociationBuilder do ) %{teams: [%{name: "Lindgren-Zemlak"}, %{name: "awesome team name"}]} - # # You can also build deep relational structures - # FactoryEx.AssociationBuilder.build_params( FactoryEx.Support.Factory.Accounts.TeamOrganization, %{}, @@ -84,7 +76,6 @@ defmodule FactoryEx.AssociationBuilder do ] } ``` - """ @doc """ @@ -106,10 +97,10 @@ defmodule FactoryEx.AssociationBuilder do schema |> fetch_assoc!(field) |> create_one_or_many_params(params, field, assoc_fields) - |> case do + |> then(fn nil -> params assoc_params -> Map.put(params, field, assoc_params) - end + end) end defp create_schema_params(schema, field, params) do @@ -117,22 +108,22 @@ defmodule FactoryEx.AssociationBuilder do end defp create_one_or_many_params( - %{cardinality: :many, queryable: queryable}, - params, - field, - assoc_fields - ) do + %{cardinality: :many, queryable: queryable}, + params, + field, + assoc_fields + ) do params |> Map.get(field, [%{}]) |> Enum.map(&factory_build(queryable, &1, assoc_fields)) end defp create_one_or_many_params( - %{cardinality: :one, queryable: queryable}, - params, - field, - assoc_fields - ) do + %{cardinality: :one, queryable: queryable}, + params, + field, + assoc_fields + ) do params = Map.get(params, field, %{}) factory_build(queryable, params, assoc_fields) end diff --git a/lib/factory_ex/factory_cache.ex b/lib/factory_ex/factory_cache.ex index e22b2b6..f5b48e1 100644 --- a/lib/factory_ex/factory_cache.ex +++ b/lib/factory_ex/factory_cache.ex @@ -4,29 +4,19 @@ defmodule FactoryEx.FactoryCache do to Factory module values. This is used to automatically generate relational ecto data structures through the `relational` option. - # Getting Started + ## Getting Started - To use this cache you must first start it with `Cache.start_link/1`. - This can be added to your test_helper.exs file, for example: + To use this cache you must first start the cache and call setup. + You can add this to your `test_helper.exs` file: ```elixir # test_helper.exs Cache.start_link([FactoryEx.FactoryCache]) + FactoryEx.FactoryCache.setup() ``` - Once the cache is started you must call `FactoryEx.FactoryCache.setup/0` - before using the relational option in your tests. - - Once the cache add the following setup before your tests: - - ```elixir - setup_all do - FactoryEx.FactoryCache.setup() - end - ``` - - In umbrella apps you can also start the cache once by checking - if the cache has already started in your `test_helper.exs` file: + In umbrella apps this may raise an error if you attempt to start multiple instances + of the cache at once. To fix this you can check if the cache has already started: ```elixir # test_helper.exs @@ -47,7 +37,7 @@ defmodule FactoryEx.FactoryCache do In umbrella applications these requirements are per application. - ## Factory Module Prefix + ## Prefix If your application's factory modules do not use the prefix `Factory` or you want to change which factory modules are loaded during tests you can configure the module prefix option at @@ -132,27 +122,47 @@ defmodule FactoryEx.FactoryCache do defp put_store(state), do: put(@store, state) defp fetch_store! do - case get(@store) do - {:ok, nil} -> - raise """ - Factories not found! + with :ok <- ensure_already_started!() do + case get(@store) do + {:ok, nil} -> + raise """ + Factories not found! - Add the following to your setup: + Add the following to your test_helper.exs: - ``` - setup do + ``` + # test_helper.exs FactoryEx.FactoryCache.setup() - end - ``` + ``` - If setup/0 is already called ensure your application meets the requirements. - See the module documentation for FactoryEx.FactoryCache for more information - or `h FactoryEx.FactoryCache` in iex. + If setup/0 is already called ensure your application meets the requirements. + See the module documentation for FactoryEx.FactoryCache for more information + or `h FactoryEx.FactoryCache` in iex. + """ - """ + {:ok, state} -> + state - {:ok, state} -> - state + end + end + end + + defp ensure_already_started! do + if already_started?() do + :ok + else + raise """ + FactoryEx.FactoryCache not started! + + The cache must be started and call setup before it can be used. + You can do this by adding the following to your test_helper.exs file: + + ``` + # test_helpers.exs + Cache.start_link([FactoryEx.FactoryCache]) + FactoryEx.FactoryCache.setup() + ``` + """ end end end diff --git a/mix.exs b/mix.exs index 18ad71d..0b81a31 100644 --- a/mix.exs +++ b/mix.exs @@ -73,6 +73,7 @@ defmodule FactoryEx.MixProject do General: [ FactoryEx, FactoryEx.SchemaCounter, + FactoryEx.AssociationBuilder, FactoryEx.FactoryCache ] ] diff --git a/test/support/datacase.ex b/test/support/datacase.ex index f7bea8d..7d6c70a 100644 --- a/test/support/datacase.ex +++ b/test/support/datacase.ex @@ -1,23 +1,7 @@ defmodule FactoryEx.DataCase do - @moduledoc """ - This module defines the setup for tests requiring - access to the application's data layer. - - You may define functions here to be used as helpers in - your tests. - - Finally, if the test case interacts with the database, - we enable the SQL sandbox, so changes done to the database - are reverted at the end of every test. If you are using - PostgreSQL, you can even run database tests asynchronously - by setting `use FactoryEx.DataCase, async: true`, although - this option is not recommended for other databases. - """ - + @moduledoc false use ExUnit.CaseTemplate - alias Ecto.Adapters.SQL.Sandbox - using do quote do alias FactoryEx.Support.Repo diff --git a/test/support/factory/accounts/label.ex b/test/support/factory/accounts/label.ex index 30fbe95..1e650bb 100644 --- a/test/support/factory/accounts/label.ex +++ b/test/support/factory/accounts/label.ex @@ -1,7 +1,5 @@ defmodule FactoryEx.Support.Factory.Accounts.Label do - @moduledoc """ - Account Test Factory - """ + @moduledoc false @behaviour FactoryEx @impl FactoryEx diff --git a/test/support/factory/accounts/role.ex b/test/support/factory/accounts/role.ex index 12cbce1..b4c2eb4 100644 --- a/test/support/factory/accounts/role.ex +++ b/test/support/factory/accounts/role.ex @@ -1,7 +1,5 @@ defmodule FactoryEx.Support.Factory.Accounts.Role do - @moduledoc """ - Account Test Factory - """ + @moduledoc false @behaviour FactoryEx @impl FactoryEx diff --git a/test/support/factory/accounts/team.ex b/test/support/factory/accounts/team.ex index 1242c85..5e476e4 100644 --- a/test/support/factory/accounts/team.ex +++ b/test/support/factory/accounts/team.ex @@ -1,7 +1,5 @@ defmodule FactoryEx.Support.Factory.Accounts.Team do - @moduledoc """ - Account Test Factory - """ + @moduledoc false @behaviour FactoryEx @impl FactoryEx diff --git a/test/support/factory/accounts/team_organization.ex b/test/support/factory/accounts/team_organization.ex index af575e9..1d50614 100644 --- a/test/support/factory/accounts/team_organization.ex +++ b/test/support/factory/accounts/team_organization.ex @@ -1,7 +1,5 @@ defmodule FactoryEx.Support.Factory.Accounts.TeamOrganization do - @moduledoc """ - Account Test Factory - """ + @moduledoc false @behaviour FactoryEx @impl FactoryEx diff --git a/test/support/factory/accounts/user.ex b/test/support/factory/accounts/user.ex index ddefaf1..97b0461 100644 --- a/test/support/factory/accounts/user.ex +++ b/test/support/factory/accounts/user.ex @@ -1,7 +1,5 @@ defmodule FactoryEx.Support.Factory.Accounts.User do - @moduledoc """ - Account Test Factory - """ + @moduledoc false @behaviour FactoryEx @impl FactoryEx From 7710b122e1e5a5402d86ab347c00cc91684004e8 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Mon, 18 Dec 2023 15:13:18 -0800 Subject: [PATCH 18/26] bump nimble_options and elixir_cache versions --- mix.exs | 4 ++-- mix.lock | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mix.exs b/mix.exs index 0b81a31..96ea102 100644 --- a/mix.exs +++ b/mix.exs @@ -45,8 +45,8 @@ defmodule FactoryEx.MixProject do {:postgrex, "~> 0.16", only: [:test, :dev], optional: true}, {:faker, ">= 0.0.0"}, - {:nimble_options, "~> 0.5"}, - {:elixir_cache, "~> 0.3.0"}, + {:nimble_options, "~> 1.0"}, + {:elixir_cache, "~> 0.3.2"}, {:credo, "~> 1.6", only: [:test, :dev], runtime: false}, {:blitz_credo_checks, "~> 0.1", only: [:test, :dev], runtime: false}, diff --git a/mix.lock b/mix.lock index 29130a1..5e77c6f 100644 --- a/mix.lock +++ b/mix.lock @@ -6,12 +6,12 @@ "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"}, "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"}, - "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"}, "ecto": {:hex, :ecto, "3.8.1", "35e0bd8c8eb772e14a5191a538cd079706ecb45164ea08a7523b4fc69ab70f56", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f1b68f8d5fe3ab89e24f57c03db5b5d0aed3602077972098b3a6006a1be4b69b"}, "ecto_sql": {:hex, :ecto_sql, "3.8.1", "1acaaba32ca0551fd19e492fc7c80414e72fc1a7140fc9395aaa53c2e8629798", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba7fc75882edce6f2ceca047315d5db27ead773cafea47f1724e35f1e7964525"}, - "elixir_cache": {:hex, :elixir_cache, "0.3.1", "da55516e4a0760760353d9e343caf8582dac18ca6d664db683aecc9a2425db60", [:mix], [{:con_cache, "~> 1.0", [hex: :con_cache, repo: "hexpm", optional: false]}, {:error_message, "~> 0.3", [hex: :error_message, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 1.2", [hex: :redix, repo: "hexpm", optional: false]}, {:sandbox_registry, "~> 0.1", [hex: :sandbox_registry, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5a4f96af7c760c0ab75ba7272b2ff24e573053c438e361033c45047d4ccb48ca"}, + "elixir_cache": {:hex, :elixir_cache, "0.3.2", "9e243d1377d325644888aad7c47a98a30197ee68a4e1ca1f394b2303672c4843", [:mix], [{:con_cache, "~> 1.0", [hex: :con_cache, repo: "hexpm", optional: false]}, {:error_message, "~> 0.3", [hex: :error_message, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 1.2", [hex: :redix, repo: "hexpm", optional: false]}, {:sandbox_registry, "~> 0.1", [hex: :sandbox_registry, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "abc42c2d83b925b5b1dc05b56dd5e98bab67c680cb01b51db04e72eb80e243ae"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "error_message": {:hex, :error_message, "0.3.2", "01fe015ba807b515ad1d9fcfcbeb49c399374393ef3fecf9de148a471198b300", [:mix], [{:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "b3c31bf0c618f7d5b812f581fdedff5829c89439134c724aab26aa958b5ad8fa"}, "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, @@ -27,17 +27,17 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"}, + "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"}, + "plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"}, "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.16.3", "fac79a81a9a234b11c44235a4494d8565303fa4b9147acf57e48978a074971db", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "aeaae1d2d1322da4e5fe90d241b0a564ce03a3add09d7270fb85362166194590"}, "redix": {:hex, :redix, "1.3.0", "f4121163ff9d73bf72157539ff23b13e38422284520bb58c05e014b19d6f0577", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "60d483d320c77329c8cbd3df73007e51b23f3fae75b7693bc31120d83ab26131"}, "sandbox_registry": {:hex, :sandbox_registry, "0.1.1", "db6a116bf8e9553111820274701e48d1971185e89b53044f47c2a8f8e74956d7", [:mix], [], "hexpm", "88e12dc6be1bb98a05439e2b1de87db4bc0c043222b4fe4b46b5da7a0cc44948"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } From 0259ec310178b710ebef05786857803781debfb3 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:38:36 -0800 Subject: [PATCH 19/26] ignore structs --- lib/factory_ex/utils.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/factory_ex/utils.ex b/lib/factory_ex/utils.ex index 8e70ae8..a55508d 100644 --- a/lib/factory_ex/utils.ex +++ b/lib/factory_ex/utils.ex @@ -209,6 +209,8 @@ defmodule FactoryEx.Utils do defp expand_many_count_tuples(count, attrs), do: Enum.map(1..count, fn _ -> expand_count_tuples(attrs) end) + defp transform({_key, %_{} = _struct} = val), do: val + defp transform({key, attrs}) when is_map(attrs), do: {key, expand_count_tuples(attrs)} defp transform({key, many_attrs}) when is_list(many_attrs) do From def44f2dad78e198346d82daab8ddc3f7a870305 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:43:27 -0800 Subject: [PATCH 20/26] support nimble options 0.4 or 1.0 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 96ea102..e65a505 100644 --- a/mix.exs +++ b/mix.exs @@ -45,7 +45,7 @@ defmodule FactoryEx.MixProject do {:postgrex, "~> 0.16", only: [:test, :dev], optional: true}, {:faker, ">= 0.0.0"}, - {:nimble_options, "~> 1.0"}, + {:nimble_options, "~> 0.4 or ~> 1.0"}, {:elixir_cache, "~> 0.3.2"}, {:credo, "~> 1.6", only: [:test, :dev], runtime: false}, From 5ad100f6b2f7ee077b696c5224d3a427eaba75cd Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:47:17 -0800 Subject: [PATCH 21/26] change elixir cache to 0.3 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index e65a505..32c7449 100644 --- a/mix.exs +++ b/mix.exs @@ -46,7 +46,7 @@ defmodule FactoryEx.MixProject do {:faker, ">= 0.0.0"}, {:nimble_options, "~> 0.4 or ~> 1.0"}, - {:elixir_cache, "~> 0.3.2"}, + {:elixir_cache, "~> 0.3"}, {:credo, "~> 1.6", only: [:test, :dev], runtime: false}, {:blitz_credo_checks, "~> 0.1", only: [:test, :dev], runtime: false}, From c7bc590a3ba80bd50bc9d8d14a2591a04565a2e1 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:26:21 -0700 Subject: [PATCH 22/26] update dep to test --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 32c7449..df97c02 100644 --- a/mix.exs +++ b/mix.exs @@ -44,7 +44,7 @@ defmodule FactoryEx.MixProject do {:ecto_sql, "~> 3.0", only: [:test, :dev], optional: true}, {:postgrex, "~> 0.16", only: [:test, :dev], optional: true}, - {:faker, ">= 0.0.0"}, + {:faker, ">= 0.0.0", only: :test}, {:nimble_options, "~> 0.4 or ~> 1.0"}, {:elixir_cache, "~> 0.3"}, From af047afc3ef0890972edfc59498ba4c4bb2d1e3c Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:34:42 -0700 Subject: [PATCH 23/26] remove deps constraints --- mix.exs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/mix.exs b/mix.exs index df97c02..59d8017 100644 --- a/mix.exs +++ b/mix.exs @@ -41,18 +41,17 @@ defmodule FactoryEx.MixProject do defp deps do [ {:ecto, "~> 3.0"}, - {:ecto_sql, "~> 3.0", only: [:test, :dev], optional: true}, - {:postgrex, "~> 0.16", only: [:test, :dev], optional: true}, - - {:faker, ">= 0.0.0", only: :test}, + {:ecto_sql, "~> 3.0", optional: true}, + {:postgrex, "~> 0.16", optional: true}, + {:faker, ">= 0.0.0"}, {:nimble_options, "~> 0.4 or ~> 1.0"}, {:elixir_cache, "~> 0.3"}, - {:credo, "~> 1.6", only: [:test, :dev], runtime: false}, - {:blitz_credo_checks, "~> 0.1", only: [:test, :dev], runtime: false}, - {:excoveralls, "~> 0.10", only: :test}, - {:ex_doc, ">= 0.0.0", optional: true, only: :dev}, - {:dialyxir, "~> 1.0", optional: true, only: :test, runtime: false} + {:credo, "~> 1.6", runtime: false}, + {:blitz_credo_checks, "~> 0.1", runtime: false}, + {:excoveralls, "~> 0.10", runtime: false}, + {:ex_doc, ">= 0.0.0", optional: true, runtime: false}, + {:dialyxir, "~> 1.0", optional: true, runtime: false} ] end From accf82718394cfa6f70cde761c8e84b715a67075 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:40:20 -0700 Subject: [PATCH 24/26] update deps based on guidelines for library handling --- mix.exs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mix.exs b/mix.exs index 59d8017..d500b5d 100644 --- a/mix.exs +++ b/mix.exs @@ -47,11 +47,11 @@ defmodule FactoryEx.MixProject do {:nimble_options, "~> 0.4 or ~> 1.0"}, {:elixir_cache, "~> 0.3"}, - {:credo, "~> 1.6", runtime: false}, - {:blitz_credo_checks, "~> 0.1", runtime: false}, - {:excoveralls, "~> 0.10", runtime: false}, - {:ex_doc, ">= 0.0.0", optional: true, runtime: false}, - {:dialyxir, "~> 1.0", optional: true, runtime: false} + {:credo, "~> 1.6", only: [:dev, :test], runtime: false, optional: true}, + {:blitz_credo_checks, "~> 0.1", only: [:dev, :test], runtime: false, optional: true}, + {:excoveralls, "~> 0.10", only: :test, runtime: false, optional: true}, + {:ex_doc, ">= 0.0.0", only: :dev, runtime: false, optional: true}, + {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false, optional: true} ] end From cf219ec36a01cf38d1cd6e7b220fef4d4eae2ffd Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Tue, 10 Sep 2024 18:12:24 -0700 Subject: [PATCH 25/26] set faker test only --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index d500b5d..bb60e57 100644 --- a/mix.exs +++ b/mix.exs @@ -43,9 +43,9 @@ defmodule FactoryEx.MixProject do {:ecto, "~> 3.0"}, {:ecto_sql, "~> 3.0", optional: true}, {:postgrex, "~> 0.16", optional: true}, - {:faker, ">= 0.0.0"}, {:nimble_options, "~> 0.4 or ~> 1.0"}, {:elixir_cache, "~> 0.3"}, + {:faker, ">= 0.0.0", only: :test}, {:credo, "~> 1.6", only: [:dev, :test], runtime: false, optional: true}, {:blitz_credo_checks, "~> 0.1", only: [:dev, :test], runtime: false, optional: true}, From af64fa32ff7df36f474c47d203319d3dfea2f316 Mon Sep 17 00:00:00 2001 From: Kurt Hogarth <87607684+cylkdev@users.noreply.github.com> Date: Tue, 10 Sep 2024 19:56:38 -0700 Subject: [PATCH 26/26] add faker to dev --- .tool-versions | 4 +-- config/config.exs | 16 +++++++++++ mix.exs | 6 ++-- mix.lock | 65 +++++++++++++++++++------------------------- test/test_helper.exs | 15 ++-------- 5 files changed, 52 insertions(+), 54 deletions(-) diff --git a/.tool-versions b/.tool-versions index 2a6a644..1fb730f 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.14.2 -erlang 25.1.2 \ No newline at end of file +elixir 1.13 +erlang 24.2.2 \ No newline at end of file diff --git a/config/config.exs b/config/config.exs index becde76..6797d28 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1 +1,17 @@ import Config + +if Mix.env() == :test do + config :factory_ex, ecto_repos: [FactoryEx.Support.Repo] + config :factory_ex, repo: FactoryEx.Support.Repo + config :factory_ex, :sql_sandbox, true + config :factory_ex, FactoryEx.Support.Repo, + username: "postgres", + password: "postgres", + database: "factory_ex_test", + hostname: "localhost", + pool: Ecto.Adapters.SQL.Sandbox, + pool_size: 10, + show_sensitive_data_on_connection_error: true, + log: :debug, + stacktrace: true +end diff --git a/mix.exs b/mix.exs index bb60e57..b686610 100644 --- a/mix.exs +++ b/mix.exs @@ -15,9 +15,9 @@ defmodule FactoryEx.MixProject do test_coverage: [tool: ExCoveralls], dialyzer: [ plt_add_apps: [:ex_unit, :mix, :credo, :ecto_sql], - list_unused_filters: true, plt_local_path: "dialyzer", plt_core_path: "dialyzer", + list_unused_filters: true, flags: [:unmatched_returns] ], preferred_cli_env: [ @@ -33,7 +33,7 @@ defmodule FactoryEx.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ - extra_applications: [:logger, :postgrex] + extra_applications: [:logger] ] end @@ -45,7 +45,7 @@ defmodule FactoryEx.MixProject do {:postgrex, "~> 0.16", optional: true}, {:nimble_options, "~> 0.4 or ~> 1.0"}, {:elixir_cache, "~> 0.3"}, - {:faker, ">= 0.0.0", only: :test}, + {:faker, ">= 0.0.0", only: [:dev, :test]}, {:credo, "~> 1.6", only: [:dev, :test], runtime: false, optional: true}, {:blitz_credo_checks, "~> 0.1", only: [:dev, :test], runtime: false, optional: true}, diff --git a/mix.lock b/mix.lock index 5e77c6f..d76c72e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,43 +1,34 @@ %{ - "blitz_credo_checks": {:hex, :blitz_credo_checks, "0.1.8", "6f4e27d1db5d0fffd6e47fa36c972fa5efca411b5bb9fc84c65b59adca39dd83", [:mix], [{:credo, "~> 1.4", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "34d4b29a8a5a83ae1d759b0819635f55160f628a9c7a77c5be9507e29360ab3b"}, - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, - "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, - "con_cache": {:hex, :con_cache, "1.0.0", "6405e2bd5d5005334af72939432783562a8c35a196c2e63108fe10bb97b366e6", [:mix], [], "hexpm", "4d1f5cb1a67f3c1a468243dc98d10ac83af7f3e33b7e7c15999dc2c9bc0a551e"}, - "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, - "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"}, - "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"}, + "blitz_credo_checks": {:hex, :blitz_credo_checks, "0.1.10", "54ae0aa673101e3edd4f2ab0ee7860f90c3240e515e268546dd9f01109d2b917", [:mix], [{:credo, "~> 1.4", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "b3248dd2c88a6fe907e84ed104e61b863c6451d8755aa609b36d3eb6c7bab9db"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "con_cache": {:hex, :con_cache, "1.1.0", "45c7c6cd6dc216e47636232e8c683734b7fe293221fccd9454fa1757bc685044", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8655f2ae13a1e56c8aef304d250814c7ed929c12810f126fc423ecc8e871593b"}, + "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, - "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"}, - "ecto": {:hex, :ecto, "3.8.1", "35e0bd8c8eb772e14a5191a538cd079706ecb45164ea08a7523b4fc69ab70f56", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f1b68f8d5fe3ab89e24f57c03db5b5d0aed3602077972098b3a6006a1be4b69b"}, - "ecto_sql": {:hex, :ecto_sql, "3.8.1", "1acaaba32ca0551fd19e492fc7c80414e72fc1a7140fc9395aaa53c2e8629798", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba7fc75882edce6f2ceca047315d5db27ead773cafea47f1724e35f1e7964525"}, - "elixir_cache": {:hex, :elixir_cache, "0.3.2", "9e243d1377d325644888aad7c47a98a30197ee68a4e1ca1f394b2303672c4843", [:mix], [{:con_cache, "~> 1.0", [hex: :con_cache, repo: "hexpm", optional: false]}, {:error_message, "~> 0.3", [hex: :error_message, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 1.2", [hex: :redix, repo: "hexpm", optional: false]}, {:sandbox_registry, "~> 0.1", [hex: :sandbox_registry, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "abc42c2d83b925b5b1dc05b56dd5e98bab67c680cb01b51db04e72eb80e243ae"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"}, + "elixir_cache": {:hex, :elixir_cache, "0.3.3", "ba90617426e866709596a477b2c433f6dddf17e136f4595a20049f4c5c3105b3", [:mix], [{:con_cache, "~> 1.0", [hex: :con_cache, repo: "hexpm", optional: false]}, {:error_message, "~> 0.3", [hex: :error_message, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 1.2", [hex: :redix, repo: "hexpm", optional: false]}, {:sandbox_registry, "~> 0.1", [hex: :sandbox_registry, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "6d92c8e2716c4254dad5dd76d4188bcf0ca3f540c706644fbed733ceaf2e710c"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "error_message": {:hex, :error_message, "0.3.2", "01fe015ba807b515ad1d9fcfcbeb49c399374393ef3fecf9de148a471198b300", [:mix], [{:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "b3c31bf0c618f7d5b812f581fdedff5829c89439134c724aab26aa958b5ad8fa"}, - "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, - "excoveralls": {:hex, :excoveralls, "0.14.5", "5c685449596e962c779adc8f4fb0b4de3a5b291c6121097572a3aa5400c386d3", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9b4a9bf10e9a6e48b94159e13b4b8a1b05400f17ac16cc363ed8734f26e1f4e"}, - "faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"}, - "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"}, + "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, - "postgrex": {:hex, :postgrex, "0.16.3", "fac79a81a9a234b11c44235a4494d8565303fa4b9147acf57e48978a074971db", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "aeaae1d2d1322da4e5fe90d241b0a564ce03a3add09d7270fb85362166194590"}, - "redix": {:hex, :redix, "1.3.0", "f4121163ff9d73bf72157539ff23b13e38422284520bb58c05e014b19d6f0577", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "60d483d320c77329c8cbd3df73007e51b23f3fae75b7693bc31120d83ab26131"}, + "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"}, + "redix": {:hex, :redix, "1.5.2", "ab854435a663f01ce7b7847f42f5da067eea7a3a10c0a9d560fa52038fd7ab48", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "78538d184231a5d6912f20567d76a49d1be7d3fca0e1aaaa20f4df8e1142dcb8"}, "sandbox_registry": {:hex, :sandbox_registry, "0.1.1", "db6a116bf8e9553111820274701e48d1971185e89b53044f47c2a8f8e74956d7", [:mix], [], "hexpm", "88e12dc6be1bb98a05439e2b1de87db4bc0c043222b4fe4b46b5da7a0cc44948"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, } diff --git a/test/test_helper.exs b/test/test_helper.exs index 518b56b..29d6138 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,17 +1,8 @@ ExUnit.start() +Faker.start() -Application.put_env(:factory_ex, :ecto_repos, [FactoryEx.Support.Repo]) - -Application.put_env(:factory_ex, :sql_sandbox, true) - -FactoryEx.Support.Repo.start_link([ - username: "postgres", - password: "postgres", - database: "factory_ex_test", - hostname: "localhost", - pool: Ecto.Adapters.SQL.Sandbox, - pool_size: 10 -]) +{:ok, _} = :application.ensure_all_started([:postgrex]) +{:ok, _} = FactoryEx.Support.Repo.start_link() Cache.start_link([FactoryEx.FactoryCache]) FactoryEx.FactoryCache.setup()