Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
cheerfulstoic committed May 11, 2023
0 parents commit 71dd18a
Show file tree
Hide file tree
Showing 12 changed files with 451 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
26 changes: 26 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
require_associations_test-*.tar

# Temporary files, for example, from tests.
/tmp/
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# EctoRequireAssociations

**TODO: Add description**

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `ecto_require_associations` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:ecto_require_associations, "~> 0.1.0"}
]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/ecto_require_associations>.

5 changes: 5 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Config

config :ecto_require_associations, Repo,
url: "postgres://postgres:example@localhost/require_associations"

13 changes: 13 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
services:
db:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: example

adminer:
image: adminer
restart: always
ports:
- 8080:8080

56 changes: 56 additions & 0 deletions lib/ecto_require_associations.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule EctoRequireAssociations do
@moduledoc """
Documentation for `RequireAssociations`.
"""

alias RequireAssociations.Associations

def ensure!(records, association_names) when is_list(records) do
Associations.paths(association_names)
|> Enum.filter(fn association_name_path ->
association_names_not_loaded?(records, association_name_path)
end)
|> case do
[] ->
:ok

[path] ->
raise ArgumentError, "Expected association to be set: #{path_to_string(path)}"

paths ->
paths_string =
paths
|> Enum.map(&path_to_string/1)
|> Enum.join(", ")

raise ArgumentError, "Expected associations to be set: #{paths_string}"
end
end

def ensure!(record, association_names), do: ensure!([record], association_names)

defp path_to_string(path) do
"`#{Enum.join(path, ".")}`"
end

defp association_names_not_loaded?(records, [association_name]) do
!Enum.all?(records, & assoc_loaded?(&1, association_name))
end

defp association_names_not_loaded?(records, [association_name | rest]) do
records
|> Enum.map(& Map.get(&1, association_name))
|> List.flatten()
|> association_names_not_loaded?(rest)
end

defp assoc_loaded?(%struct_mod{} = record, association_name) do
if Map.has_key?(struct(struct_mod), association_name) do
record
|> Map.get(association_name)
|> Ecto.assoc_loaded?()
else
raise ArgumentError, "Association `#{association_name}` is not defined for the `#{Macro.to_string(struct_mod)}` struct"
end
end
end
52 changes: 52 additions & 0 deletions lib/require_associations/associations.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule RequireAssociations.Associations do
@doc """
Turns a set of associations (as used by Ecto's [preload](https://hexdocs.pm/ecto/Ecto.Repo.html#c:preload/3))
into a list of paths to that association. Examples are most helpful for understanding this. Imagine we have
the following schemas:
defmodule User do
use Ecto.Schema
schema "users" do
belongs_to :region, Region
has_many :roles, Role
end
end
defmodule Role do
use Ecto.Schema
schema "roles" do
belongs_to :region, Region
end
end
iex> RequireAssociations.Associations.paths(:region)
[[:region]]
iex> RequireAssociations.Associations.paths([:region, :roles])
[[:region], [:roles]]
iex> RequireAssociations.Associations.paths([:region, roles: :region])
[[:region], [:roles], [:roles, :region]]
"""

def paths(definitions), do: do_paths(definitions, [])

defp do_paths(definition, prefix) when is_atom(definition), do: [prefix ++ [definition]]
defp do_paths(definition, prefix) when is_binary(definition), do: do_paths(String.to_atom(definition), prefix)

defp do_paths(definitions, prefix) when is_list(definitions) do
Enum.flat_map(definitions, fn
{key, value} ->
new_prefix = prefix ++ [key]

[new_prefix] ++ do_paths(value, new_prefix)

definition ->
do_paths(definition, prefix)
end)
end
end

45 changes: 45 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
defmodule RequireAssociations.MixProject do
use Mix.Project

def project do
[
app: :ecto_require_associations,
description: "Tool for validating that Ecto associations have been set",
version: "0.1.0",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: deps(),
package: package(),
preferred_cli_env: [
"test.watch": :test
],
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

defp package() do
[
# This option is only needed when you don't want to use the OTP application name
name: "ecto_require_associations",
# These are the default files included in the package
licenses: ["MIT"],
links: %{"GitHub" => "https://github.com/cheerfulstoic/require_associations_test"}
]
end


# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:mix_test_watch, "~> 1.0", only: [:dev, :test], runtime: false},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
{:ecto, "~> 3.10"}
]
end
end
13 changes: 13 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
%{
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"},
"ecto": {:hex, :ecto, "3.10.1", "c6757101880e90acc6125b095853176a02da8f1afe056f91f1f90b80c9389822", [: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", "d2ac4255f1601bdf7ac74c0ed971102c6829dc158719b94bd30041bbad77f87a"},
"ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [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", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"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.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
}
Loading

0 comments on commit 71dd18a

Please sign in to comment.