diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..f078317 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,179 @@ +# This file is synced with beam-community/common-config. Any changes will be overwritten. + +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: ["config/", "lib/", "priv/", "test/"], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: true, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + {Credo.Check.Consistency.UnusedVariableNames, false}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 2]}, + {Credo.Check.Design.DuplicatedCode, false}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + {Credo.Check.Design.TagFIXME, []}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasAs, false}, + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.LargeNumbers, [trailing_digits: 2]}, + {Credo.Check.Readability.MaxLineLength, false}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, false}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.MultiAlias, false}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.Specs, false}, + {Credo.Check.Readability.StrictModuleLayout, + [ + order: + ~w(moduledoc behaviour use import require alias module_attribute defstruct callback macrocallback optional_callback)a, + ignore: [:type] + ]}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.ABCSize, false}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + # {Credo.Check.Refactor.MapInto, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.ModuleDependencies, false}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.VariableRebinding, false}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + # {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + # disabling this check by default, as if not included, it will be + # run on version 1.7.0 and above + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, false}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnsafeToAtom, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []} + ] + } + ] +} diff --git a/.formatter.exs b/.formatter.exs index eef1485..e5aed66 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,3 +1,5 @@ +# This file is synced with beam-community/common-config. Any changes will be overwritten. + [ import_deps: [:ecto, :ecto_sql], inputs: ["*.{heex,ex,exs}", "{config,lib,priv,test}/**/*.{heex,ex,exs}"], diff --git a/.github/release-please-config.json b/.github/release-please-config.json new file mode 100644 index 0000000..cd8b23a --- /dev/null +++ b/.github/release-please-config.json @@ -0,0 +1,39 @@ +{ + "$comment": "This file is synced with beam-community/common-config. Any changes will be overwritten.", + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "changelog-sections": [ + { + "type": "feat", + "section": "Features", + "hidden": false + }, + { + "type": "fix", + "section": "Bug Fixes", + "hidden": false + }, + { + "type": "chore", + "section": "Miscellaneous", + "hidden": false + } + ], + "draft": false, + "draft-pull-request": false, + "packages": { + ".": { + "extra-files": [ + "README.md" + ], + "release-type": "elixir" + } + }, + "plugins": [ + { + "type": "sentence-case" + } + ], + "prerelease": false, + "pull-request-header": "An automated release has been created for you.", + "separate-pull-requests": true +} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..bebad11 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,116 @@ +# This file is synced with beam-community/common-config. Any changes will be overwritten. + +name: CI + +on: + merge_group: + pull_request: + types: + - opened + - reopened + - synchronize + push: + branches: + - main + workflow_call: + secrets: + GH_PERSONAL_ACCESS_TOKEN: + required: true + workflow_dispatch: + +concurrency: + group: CI ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + Credo: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Elixir + uses: stordco/actions-elixir/setup@v1 + with: + github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} + + - name: Credo + run: mix credo --strict + + Dependencies: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Elixir + uses: stordco/actions-elixir/setup@v1 + with: + github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} + + - name: Unused + run: mix deps.unlock --check-unused + + Dialyzer: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Elixir + uses: stordco/actions-elixir/setup@v1 + with: + github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} + + - name: Dialyzer + run: mix dialyzer --format github + + Format: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Elixir + uses: stordco/actions-elixir/setup@v1 + with: + github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} + + - name: Format + run: mix format --check-formatted + + Test: + runs-on: ubuntu-latest + + env: + MIX_ENV: test + + services: + postgres: + image: postgres:16-alpine + ports: + - 5432:5432 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_INITDB_ARGS: "--nosync" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Elixir + uses: stordco/actions-elixir/setup@v1 + with: + github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} + + - name: Compile + run: mix compile --warnings-as-errors + + - name: Test + run: mix test + diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml new file mode 100644 index 0000000..5e2628a --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,35 @@ +# This file is synced with beam-community/common-config. Any changes will be overwritten. + +name: PR + +on: + merge_group: + pull_request: + types: + - edited + - opened + - reopened + - synchronize + +jobs: + Title: + if: ${{ github.event_name == 'pull_request' }} + name: Check Title + runs-on: ubuntu-latest + + steps: + - name: Check + uses: stordco/actions-pr-title@v1.0.0 + with: + regex: '^(feat!|fix!|fix|feat|chore)(\(\w+\))?:\s(\[#\d{1,5}\])?.*$' + hint: | + Your PR title does not match the Conventional Commits convention. Please rename your PR to match one of the following formats: + + fix: [#123] some title of the PR + fix(scope): [#123] some title of the PR + feat: [#1234] some title of the PR + chore: update some action + + Note: Adding ! (i.e. `feat!:`) represents a breaking change and will result in a SemVer major release. + + See https://www.conventionalcommits.org/en/v1.0.0/ for more information. diff --git a/.github/workflows/production.yaml b/.github/workflows/production.yaml new file mode 100644 index 0000000..2a6b98f --- /dev/null +++ b/.github/workflows/production.yaml @@ -0,0 +1,15 @@ +# This file is synced with beam-community/common-config. Any changes will be overwritten. + +name: Production + +on: + release: + types: + - released + - prereleased + workflow_dispatch: + +concurrency: + group: Production + +jobs: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..40ef723 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,23 @@ +# This file is synced with beam-community/common-config. Any changes will be overwritten. + +name: Release + +on: + push: + branches: + - main + +jobs: + Please: + runs-on: ubuntu-latest + + steps: + - id: release + name: Release + uses: google-github-actions/release-please-action@v3 + with: + command: manifest + config-file: .github/release-please-config.json + manifest-file: .github/release-please-manifest.json + release-type: elixir + token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} diff --git a/lib/ex_machina.ex b/lib/ex_machina.ex index 3a8f7ff..04be659 100644 --- a/lib/ex_machina.ex +++ b/lib/ex_machina.ex @@ -4,30 +4,16 @@ defmodule ExMachina do In depth examples are in the [README](readme.html) """ + use Application - defmodule UndefinedFactoryError do - @moduledoc """ - Error raised when trying to build or create a factory that is undefined. - """ - - defexception [:message] - - def exception(factory_name) do - message = """ - No factory defined for #{inspect(factory_name)}. - - Please check for typos or define your factory: - - def #{factory_name}_factory do - ... - end - """ - - %UndefinedFactoryError{message: message} - end - end + alias ExMachina.UndefinedFactoryError - use Application + @callback build(factory_name :: atom) :: any + @callback build(factory_name :: atom, attrs :: keyword | map) :: any + @callback build_list(number_of_records :: integer, factory_name :: atom) :: list + @callback build_list(number_of_records :: integer, factory_name :: atom, attrs :: keyword | map) :: list + @callback build_pair(factory_name :: atom) :: list + @callback build_pair(factory_name :: atom, attrs :: keyword | map) :: list @doc false def start(_type, _args), do: ExMachina.Sequence.start_link() @@ -45,6 +31,8 @@ defmodule ExMachina do evaluate_lazy_attributes: 1 ] + alias ExMachina.UndefinedFactoryError + def build(factory_name, attrs \\ %{}) do ExMachina.build(__MODULE__, factory_name, attrs) end @@ -221,10 +209,6 @@ defmodule ExMachina do # Returns %Article{title: "hello world", slug: "hello-world"} build(:article, title: "hello world") """ - @callback build(factory_name :: atom) :: any - @callback build(factory_name :: atom, attrs :: keyword | map) :: any - - @doc false def build(module, factory_name, attrs \\ %{}) do attrs = Enum.into(attrs, %{}) @@ -235,7 +219,8 @@ defmodule ExMachina do apply(module, function_name, [attrs]) factory_without_attributes_defined?(module, function_name) -> - apply(module, function_name, []) + module + |> apply(function_name, []) |> merge_attributes(attrs) |> evaluate_lazy_attributes() @@ -248,6 +233,7 @@ defmodule ExMachina do factory_name |> Atom.to_string() |> Kernel.<>("_factory") + # credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom |> String.to_atom() end @@ -343,10 +329,6 @@ defmodule ExMachina do # Returns a list of 2 users build_pair(:user) """ - @callback build_pair(factory_name :: atom) :: list - @callback build_pair(factory_name :: atom, attrs :: keyword | map) :: list - - @doc false def build_pair(module, factory_name, attrs \\ %{}) do ExMachina.build_list(module, 2, factory_name, attrs) end @@ -359,16 +341,13 @@ defmodule ExMachina do # Returns a list of 3 users build_list(3, :user) """ - @callback build_list(number_of_records :: integer, factory_name :: atom) :: list - @callback build_list(number_of_records :: integer, factory_name :: atom, attrs :: keyword | map) :: - list - - @doc false def build_list(module, number_of_records, factory_name, attrs \\ %{}) do - Stream.repeatedly(fn -> - ExMachina.build(module, factory_name, attrs) - end) - |> Enum.take(number_of_records) + stream = + Stream.repeatedly(fn -> + ExMachina.build(module, factory_name, attrs) + end) + + Enum.take(stream, number_of_records) end defmacro __before_compile__(_env) do diff --git a/lib/ex_machina/ecto.ex b/lib/ex_machina/ecto.ex index de6f143..7a1bded 100644 --- a/lib/ex_machina/ecto.ex +++ b/lib/ex_machina/ecto.ex @@ -12,43 +12,7 @@ defmodule ExMachina.Ecto do More in-depth examples are in the [README](readme.html). """ - defmacro __using__(opts) do - verify_ecto_dep() - - quote do - use ExMachina - use ExMachina.EctoStrategy, repo: unquote(Keyword.get(opts, :repo)) - - def params_for(factory_name, attrs \\ %{}) do - ExMachina.Ecto.params_for(__MODULE__, factory_name, attrs) - end - - def string_params_for(factory_name, attrs \\ %{}) do - ExMachina.Ecto.string_params_for(__MODULE__, factory_name, attrs) - end - - def params_with_assocs(factory_name, attrs \\ %{}) do - ExMachina.Ecto.params_with_assocs(__MODULE__, factory_name, attrs) - end - - def string_params_with_assocs(factory_name, attrs \\ %{}) do - ExMachina.Ecto.string_params_with_assocs(__MODULE__, factory_name, attrs) - end - end - end - - defp verify_ecto_dep do - unless Code.ensure_loaded?(Ecto) do - raise "You tried to use ExMachina.Ecto, but the Ecto module is not loaded. " <> - "Please add ecto to your dependencies." - end - end - @doc """ - Builds a factory and inserts it into the database. - - The arguments are the same as `c:ExMachina.build/2`. - """ @callback insert(factory_name :: atom) :: any @callback insert(factory_name :: atom, attrs :: keyword | map) :: any @@ -121,13 +85,6 @@ defmodule ExMachina.Ecto do @callback params_for(factory_name :: atom) :: %{optional(atom) => any} @callback params_for(factory_name :: atom, attrs :: keyword | map) :: %{optional(atom) => any} - @doc false - def params_for(module, factory_name, attrs \\ %{}) do - factory_name - |> module.build(attrs) - |> recursively_strip - end - @doc """ Similar to `c:params_for/2` but converts atom keys to strings in returned map. @@ -148,13 +105,6 @@ defmodule ExMachina.Ecto do optional(String.t()) => any } - @doc false - def string_params_for(module, factory_name, attrs \\ %{}) do - module - |> params_for(factory_name, attrs) - |> convert_atom_keys_to_strings - end - @doc """ Similar to `c:params_for/2` but inserts all `belongs_to` associations and sets the foreign keys. @@ -174,15 +124,6 @@ defmodule ExMachina.Ecto do @callback params_with_assocs(factory_name :: atom, attrs :: keyword | map) :: %{ optional(atom) => any } - - @doc false - def params_with_assocs(module, factory_name, attrs \\ %{}) do - factory_name - |> module.build(attrs) - |> insert_belongs_to_assocs(module) - |> recursively_strip - end - @doc """ Similar to `c:params_with_assocs/2` but converts atom keys to strings in returned map. @@ -204,6 +145,53 @@ defmodule ExMachina.Ecto do optional(String.t()) => any } + defmacro __using__(opts) do + verify_ecto_dep() + + quote do + use ExMachina + use ExMachina.EctoStrategy, repo: unquote(Keyword.get(opts, :repo)) + + def params_for(factory_name, attrs \\ %{}) do + ExMachina.Ecto.params_for(__MODULE__, factory_name, attrs) + end + + def string_params_for(factory_name, attrs \\ %{}) do + ExMachina.Ecto.string_params_for(__MODULE__, factory_name, attrs) + end + + def params_with_assocs(factory_name, attrs \\ %{}) do + ExMachina.Ecto.params_with_assocs(__MODULE__, factory_name, attrs) + end + + def string_params_with_assocs(factory_name, attrs \\ %{}) do + ExMachina.Ecto.string_params_with_assocs(__MODULE__, factory_name, attrs) + end + end + end + + @doc false + def params_for(module, factory_name, attrs \\ %{}) do + factory_name + |> module.build(attrs) + |> recursively_strip + end + + @doc false + def string_params_for(module, factory_name, attrs \\ %{}) do + module + |> params_for(factory_name, attrs) + |> convert_atom_keys_to_strings + end + + @doc false + def params_with_assocs(module, factory_name, attrs \\ %{}) do + factory_name + |> module.build(attrs) + |> insert_belongs_to_assocs(module) + |> recursively_strip + end + @doc false def string_params_with_assocs(module, factory_name, attrs \\ %{}) do module @@ -223,7 +211,9 @@ defmodule ExMachina.Ecto do defp recursively_strip(record), do: record defp handle_assocs(%{__struct__: struct} = record) do - Enum.reduce(struct.__schema__(:associations), record, fn association_name, record -> + associations = struct.__schema__(:associations) + + Enum.reduce(associations, record, fn association_name, record -> case struct.__schema__(:association, association_name) do %{__struct__: Ecto.Association.BelongsTo} -> Map.delete(record, association_name) @@ -255,7 +245,9 @@ defmodule ExMachina.Ecto do end defp handle_embeds(%{__struct__: struct} = record) do - Enum.reduce(struct.__schema__(:embeds), record, fn embed_name, record -> + embeds = struct.__schema__(:embeds) + + Enum.reduce(embeds, record, fn embed_name, record -> record |> Map.get(embed_name) |> handle_embed(record, embed_name) @@ -278,21 +270,15 @@ defmodule ExMachina.Ecto do end defp set_persisted_belongs_to_ids(%{__struct__: struct} = record) do - Enum.reduce(struct.__schema__(:associations), record, fn association_name, record -> - association = struct.__schema__(:association, association_name) + associations = struct.__schema__(:associations) - case association do - %{__struct__: Ecto.Association.BelongsTo} -> - case Map.get(record, association_name) do - belongs_to = %{__meta__: %{__struct__: Ecto.Schema.Metadata, state: :loaded}} -> - set_belongs_to_primary_key(record, belongs_to, association) - - _ -> - record - end + Enum.reduce(associations, record, fn association_name, record -> + association = struct.__schema__(:association, association_name) - _ -> - record + with %{__struct__: Ecto.Association.BelongsTo} <- association, + %{__meta__: %{__struct__: Ecto.Schema.Metadata, state: :loaded}} = belongs_to <- + Map.get(record, association_name) do + set_belongs_to_primary_key(record, belongs_to, association) end end) end @@ -303,7 +289,9 @@ defmodule ExMachina.Ecto do end defp insert_belongs_to_assocs(%{__struct__: struct} = record, module) do - Enum.reduce(struct.__schema__(:associations), record, fn association_name, record -> + assocations = struct.__schema__(:associations) + + Enum.reduce(assocations, record, fn association_name, record -> case struct.__schema__(:association, association_name) do association = %{__struct__: Ecto.Association.BelongsTo} -> insert_built_belongs_to_assoc(module, association, record) @@ -354,19 +342,23 @@ defmodule ExMachina.Ecto do end defp convert_atom_keys_to_strings(%NaiveDateTime{} = value) do - if Application.get_env(:ex_machina, :preserve_dates, false), - do: value, - else: Map.from_struct(value) |> convert_atom_keys_to_strings() + if Application.get_env(:ex_machina, :preserve_dates, false) do + value + else + value |> Map.from_struct() |> convert_atom_keys_to_strings() + end end defp convert_atom_keys_to_strings(%DateTime{} = value) do - if Application.get_env(:ex_machina, :preserve_dates, false), - do: value, - else: Map.from_struct(value) |> convert_atom_keys_to_strings() + if Application.get_env(:ex_machina, :preserve_dates, false) do + value + else + value |> Map.from_struct() |> convert_atom_keys_to_strings() + end end defp convert_atom_keys_to_strings(%{__struct__: _} = record) when is_map(record) do - Map.from_struct(record) |> convert_atom_keys_to_strings() + record |> Map.from_struct() |> convert_atom_keys_to_strings() end defp convert_atom_keys_to_strings(record) when is_map(record) do @@ -376,4 +368,11 @@ defmodule ExMachina.Ecto do end defp convert_atom_keys_to_strings(value), do: value + + defp verify_ecto_dep do + unless Code.ensure_loaded?(Ecto) do + raise "You tried to use ExMachina.Ecto, but the Ecto module is not loaded. " <> + "Please add ecto to your dependencies." + end + end end diff --git a/lib/ex_machina/strategy.ex b/lib/ex_machina/strategy.ex index 3ca22df..2fb4fa7 100644 --- a/lib/ex_machina/strategy.ex +++ b/lib/ex_machina/strategy.ex @@ -1,3 +1,4 @@ +# credo:disable-for-this-file Credo.Check.Warning.UnsafeToAtom defmodule ExMachina.Strategy do @moduledoc ~S""" Module for making new strategies for working with factories @@ -74,7 +75,8 @@ defmodule ExMachina.Strategy do def unquote(function_name)(already_built_record, function_opts) when is_map(already_built_record) do opts = - Map.new(unquote(opts)) + unquote(opts) + |> Map.new() |> Map.merge(%{factory_module: __MODULE__}) apply( @@ -85,7 +87,7 @@ defmodule ExMachina.Strategy do end def unquote(function_name)(already_built_record) when is_map(already_built_record) do - opts = Map.new(unquote(opts)) |> Map.merge(%{factory_module: __MODULE__}) + opts = unquote(opts) |> Map.new() |> Map.merge(%{factory_module: __MODULE__}) apply( unquote(custom_strategy_module), @@ -121,17 +123,21 @@ defmodule ExMachina.Strategy do end def unquote(:"#{function_name}_list")(number_of_records, factory_name, attrs, opts) do - Stream.repeatedly(fn -> - unquote(function_name)(factory_name, attrs, opts) - end) - |> Enum.take(number_of_records) + stream = + Stream.repeatedly(fn -> + unquote(function_name)(factory_name, attrs, opts) + end) + + Enum.take(stream, number_of_records) end def unquote(:"#{function_name}_list")(number_of_records, factory_name, attrs \\ %{}) do - Stream.repeatedly(fn -> - unquote(function_name)(factory_name, attrs) - end) - |> Enum.take(number_of_records) + stream = + Stream.repeatedly(fn -> + unquote(function_name)(factory_name, attrs) + end) + + Enum.take(stream, number_of_records) end end end diff --git a/lib/ex_machina/undefined_factory_error.ex b/lib/ex_machina/undefined_factory_error.ex new file mode 100644 index 0000000..5cb2ea0 --- /dev/null +++ b/lib/ex_machina/undefined_factory_error.ex @@ -0,0 +1,21 @@ +defmodule ExMachina.UndefinedFactoryError do + @moduledoc """ + Error raised when trying to build or create a factory that is undefined. + """ + + defexception [:message] + + def exception(factory_name) do + message = """ + No factory defined for #{inspect(factory_name)}. + + Please check for typos or define your factory: + + def #{factory_name}_factory do + ... + end + """ + + %__MODULE__{message: message} + end +end diff --git a/mix.lock b/mix.lock index 287f99c..0e1f4c6 100644 --- a/mix.lock +++ b/mix.lock @@ -1,10 +1,8 @@ %{ - "benchee": {:hex, :benchee, "1.3.0", "f64e3b64ad3563fa9838146ddefb2d2f94cf5b473bdfd63f5ca4d0657bf96694", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "34f4294068c11b2bd2ebf2c59aac9c7da26ffa0068afdf3419f1b176e16c5f81"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "credo": {:hex, :credo, "1.7.3", "05bb11eaf2f2b8db370ecaa6a6bda2ec49b2acd5e0418bc106b73b07128c0436", [: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", "35ea675a094c934c22fb1dca3696f3c31f2728ae6ef5a53b5d648c11180a4535"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, - "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, @@ -20,6 +18,5 @@ "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [: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", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"}, - "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, } diff --git a/test/ex_machina/ecto_strategy_test.exs b/test/ex_machina/ecto_strategy_test.exs index d8888eb..35b712d 100644 --- a/test/ex_machina/ecto_strategy_test.exs +++ b/test/ex_machina/ecto_strategy_test.exs @@ -133,7 +133,7 @@ defmodule ExMachina.EctoStrategyTest do refute Enum.member?(publisher_fields, :name) - publisher = Map.merge(TestFactory.build(:publisher), %{name: "name"}) + publisher = :publisher |> TestFactory.build() |> Map.merge(%{name: "name"}) model = TestFactory.insert(:article, publisher: publisher) assert model.publisher.name == "name" @@ -157,11 +157,11 @@ defmodule ExMachina.EctoStrategyTest do assert without_args.id assert without_args.db_value - with_struct = TestFactory.build(:user) |> TestFactory.insert(returning: true) + with_struct = :user |> TestFactory.build() |> TestFactory.insert(returning: true) assert with_struct.id assert with_struct.db_value - without_opts = TestFactory.build(:user) |> TestFactory.insert() + without_opts = :user |> TestFactory.build() |> TestFactory.insert() assert without_opts.id refute without_opts.db_value end @@ -200,7 +200,7 @@ defmodule ExMachina.EctoStrategyTest do message = ~r/You called `insert` on a record that has already been inserted./ assert_raise RuntimeError, message, fn -> - TestFactory.insert(:user, name: "Maximus") |> TestFactory.insert() + :user |> TestFactory.insert(name: "Maximus") |> TestFactory.insert() end end end diff --git a/test/ex_machina/ecto_test.exs b/test/ex_machina/ecto_test.exs index b58e126..3ddccf4 100644 --- a/test/ex_machina/ecto_test.exs +++ b/test/ex_machina/ecto_test.exs @@ -53,7 +53,7 @@ defmodule ExMachina.EctoTest do describe "insert/2 insert_pair/2 insert_list/3" do test "insert, insert_pair and insert_list inserts records" do - assert %User{} = TestFactory.build(:user) |> TestFactory.insert() + assert %User{} = :user |> TestFactory.build() |> TestFactory.insert() assert %User{} = TestFactory.insert(:user) assert %User{} = TestFactory.insert(:user, admin: true) @@ -324,6 +324,6 @@ defmodule ExMachina.EctoTest do end defp has_association_in_schema?(model, association_name) do - Enum.member?(model.__schema__(:associations), association_name) + :associations |> model.__schema__() |> Enum.member?(association_name) end end diff --git a/test/ex_machina/strategy_test.exs b/test/ex_machina/strategy_test.exs index b11e18b..4cf5668 100644 --- a/test/ex_machina/strategy_test.exs +++ b/test/ex_machina/strategy_test.exs @@ -34,7 +34,7 @@ defmodule ExMachina.StrategyTest do strategy_options = %{foo: :bar, factory_module: JsonFactory} function_options = [encode: true] - JsonFactory.build(:user) |> JsonFactory.json_encode() + :user |> JsonFactory.build() |> JsonFactory.json_encode() built_user = JsonFactory.build(:user) assert_received {:handle_json_encode, ^built_user, ^strategy_options} refute_received {:handle_json_encode, _, _} diff --git a/test/support/models/invalid_type.ex b/test/support/models/invalid_type.ex index 30989fa..29bbfef 100644 --- a/test/support/models/invalid_type.ex +++ b/test/support/models/invalid_type.ex @@ -2,6 +2,8 @@ defmodule ExMachina.InvalidType do @behaviour Ecto.Type def type, do: :integer + def equal?(_a, _b), do: false + def embed_as(_), do: :self def cast(_), do: :error def load(_), do: :error