Current section
Files
Jump to
Current section
Files
lib/ecto_reflection.ex
defmodule EctoReflection do
import Enum, only: [map: 2]
@moduledoc """
Provides some conveniences to work with Queries and Schemas
"""
@spec schemas(atom()) :: list(module())
@doc """
Return a list of all modules that use `Ecto.Schema` in the application.
This is determined by checking if the `__schema__` function is defined on that module
"""
def schemas(application) do
{:ok, modules} = :application.get_key(application, :modules)
for module <- modules, defines_schema?(module), do: module
end
@spec attributes(Ecto.Schema) :: list(atom())
@doc """
Return a list of attributes for an `Ecto.Schema`.
If we have the following `Ecto.Schema`
```
defmodule Foo do
use Ecto.Schema
schema "foos" do
field :bars
field :bazes
timestamps()
end
end
```
We can do:
```
iex> EctoReflection.attributes(Foo)
~w[bars bazes id inserted_at password updated_at]a
```
"""
def attributes(module) do
module.__struct__
|> Map.keys()
|> Kernel.--(~w[__meta__ __struct__]a)
end
@spec attributes(Ecto.Schema, :binary) :: list(binary())
@doc """
Return a list of attributes as `binary()` for an `Ecto.Schema`
```
iex> EctoReflection.attributes(Foo, :binary)
~w[bars bazes id inserted_at password updated_at]
```
"""
def attributes(module, :binary) do
module
|> attributes()
|> map(&to_string/1)
end
@spec attribute?(Ecto.Schema, atom() | binary() ):: boolean()
@doc """
Check if an attribute exists on an `Ecto.Schema`.
```
iex> EctoReflection.attribute?(Foo, :bars)
true
iex> EctoReflection.attribute?(Foo, :non_attribute)
false
```
You can also safely use a binary to check:
```
iex> EctoReflection.attribute?(Foo, "bars")
true
iex> EctoReflection.attribute?(Foo, "non_attribute")
false
```
"""
def attribute?(module, key) do
to_string(key) in (attributes(module, :binary))
end
@spec fields(Ecto.Schema) :: list(atom())
@doc """
Return a list of fields (virtual or non virtual) defined in an `Ecto.Schema`
```
iex> EctoReflection.fields(Foo)
~w[bars bazes id inserted_at password updated_at]a
```
"""
def fields(module) do
module.__struct__
|> Map.keys()
|> Kernel.--(~w[__meta__ __struct__]a)
|> Kernel.--(module.__schema__(:associations))
|> Kernel.--(module.__schema__(:embeds))
end
@spec fields(Ecto.Schema, :binary) :: list(binary())
@doc """
Return a list of fields as binaries (virtual or non virtual) defined in an `Ecto.Schema`
```
iex> EctoReflection.fields(Foo)
~w[bars bazes id inserted_at password updated_at]a
```
"""
def fields(module, :binary) do
module
|> fields()
|> map(&to_string/1)
end
@spec field?(Ecto.Schema, atom() | binary()) :: boolean()
@doc """
Check if a field is defined on a schema
```
iex> EctoReflection.field?(Foo, :bars)
true
iex> EctoReflection.field?(Foo, :non_field)
false
```
You can also safely pass in a binary
```
iex> EctoReflection.field?(Foo, "bars")
true
iex> EctoReflection.field?(Foo, "non_field")
false
```
"""
def field?(module, key) do
to_string(key) in fields(module, :binary)
end
@spec types(Ecto.Schema) :: map
@doc """
Return a map of the schema and its types as a convenient shorthand
Say we have the following User `Ecto.Schema`
```
defmodule User do
use Ecto.Schema
schema "users" do
field(:username, :string)
field(:email, :string)
field(:age, :integer)
field(:password, :string, virtual: true)
field(:password_digest, :string)
has_one(:profile, Profile)
many_to_many(:addresses, Address, join_through: AddressUser)
has_many(:projects, Project)
has_many(:todos, through: [:projects, :todos])
timestamps()
end
end
```
Its types would look like this:
```
iex> EctoReflection.types(User)
%{
addresses: {:many_to_many, Address, AddressUser},
age: {:field, :integer},
email: {:field, :string},
id: {:field, :id},
inserted_at: {:field, :naive_datetime},
password: {:virtual_field, :string},
password_digest: {:field, :string},
profile: {:has_one, Profile},
projects: {:has_many, Project},
todos: {:has_many_through, Todo, [:projects, :todos]},
updated_at: {:field, :naive_datetime},
username: {:field, :string}
}
```
"""
def types(module) do
module
|> attributes()
|> Enum.into(%{}, fn(key) -> {key, type(module, key)} end)
end
@spec type(Ecto.Schema, atom() | binary()) :: {atom(), atom() | module()} | {atom(), module(), module() | [atom()] }
@doc """
Return the type for a `Ecto.Schema`'s attribute
Accepts an atom or a binary for the attribute
```
iex> EctoReflection.type(User, :projects)
{:has_many, Project}
```
"""
def type(module, key) when is_binary(key) do
if attribute?(module, key) do
type(module, String.to_atom(key))
end
end
def type(schema, key) when is_atom(key) do
with(
nil <- schema.__schema__(:type, key),
nil <- schema.__schema__(:association, key),
nil <- schema.__schema__(:embed, key),
type when not is_nil(type) <- schema.__changeset__ |> Map.get(key)
) do
{:virtual, type}
end
|> data_type(schema)
end
@spec source_fields(Ecto.Schema) :: list(atom())
@doc """
Return a list of fields that to the database.
In other words, all non-virtual fields
```
iex> EctoReflection.source_fields(Foo)
~w[id bars bazes inserted_at updated_at]a
```
"""
def source_fields(module) do
module.__schema__(:fields)
end
@spec source_fields(Ecto.Schema, :binary) :: list(binary())
@spec source_fields(Ecto.Schema) :: list(atom())
@doc """
Return a list of fields that to the database as binaries.
In other words, all non-virtual fields
```
iex> EctoReflection.source_fields(Foo, :binary)
~w[id bars bazes inserted_at updated_at]
```
"""
def source_fields(module, :binary) do
module
|> source_fields()
|> map(&to_string/1)
end
@spec source_field?(Ecto.Schema, atom() | binary()) :: boolean()
@doc """
Check if a non-virtual field is defined on a schema
```
iex> EctoReflection.source_field?(Foo, :bars)
true
iex> EctoReflection.source_field?(Foo, :password)
false
```
Also safe with binaries
```
iex> EctoReflection.source_field?(Foo, "bars")
true
iex> EctoReflection.source_field?(Foo, "password")
false
```
"""
def source_field?(module, key) do
to_string(key) in source_fields(module, :binary)
end
@spec virtual_fields(Ecto.Schema) :: list(atom())
@doc """
Return a list of virtual fields defined on a schema
```
iex> EctoReflection.virtual_fields(Foo)
~w[password]a
```
"""
def virtual_fields(module) do
fields(module) -- source_fields(module)
end
@spec virtual_fields(Ecto.Schema, :binary) :: list(binary())
@doc """
Return a list of virtual fields defined on a schema as binaries
```
iex> EctoReflection.virtual_fields(Foo, :binary)
~w[password]
```
"""
def virtual_fields(module, :binary) do
module
|> virtual_fields()
|> map(&to_string/1)
end
@spec virtual_field?(Ecto.Schema, atom() | binary()) :: boolean()
@doc """
Check if the virtual field exists on the `Ecto.Schema`
```
iex> EctoReflection.virtual_field?(Foo, :password)
true
iex> EctoReflection.virtual_field?(Foo, :bars)
false
```
Also works with binaries
```
iex> EctoReflection.virtual_field?(Foo, "password")
true
iex> EctoReflection.virtual_field?(Foo, "bars")
false
```
"""
def virtual_field?(module, key) do
to_string(key) in virtual_fields(module, :binary)
end
@spec associations(Ecto.Schema) :: list(atom())
@doc """
List all associations defined in a `Ecto.Schema`
```
iex> EctoReflection.associations(User)
~w[profile addresses projects todos]a
```
"""
def associations(module) do
module.__schema__(:associations)
end
@spec associations(Ecto.Schema, :binary) :: list(binary())
@doc """
List all associations defined in a `Ecto.Schema` as binaries
```
iex> EctoReflection.associations(User, :binary)
~w[profile addresses projects todos]
```
"""
def associations(module, :binary) do
module
|> associations()
|> map(&to_string/1)
end
@spec association?(Ecto.Schema, atom() | binary()) :: boolean()
@doc """
Check if the association is defined on the `Ecto.Schema`
```
iex> EctoReflection.association?(User, :projects)
true
iex> EctoReflection.association?(User, :unassociated)
false
```
Also safe with binaries
```
iex> EctoReflection.association?(User, "projects")
true
iex> EctoReflection.association?(User, "unassociated")
false
```
"""
def association?(module, key) do
to_string(key) in associations(module, :binary)
end
@spec embeds(Ecto.Schema) :: list(atom())
def embeds(module) do
module.__schema__(:embeds)
end
@spec embeds(Ecto.Schema, :binary) :: list(binary())
def embeds(module, :binary) do
module
|> embeds()
|> map(&to_string/1)
end
@spec embed?(Ecto.Schema, atom() | binary()) :: boolean()
def embed?(module, key) do
to_string(key) in embeds(module, :binary)
end
@spec relationships(Ecto.Schema) :: list(atom())
def relationships(module) do
associations(module) ++ embeds(module)
end
@spec relationships(Ecto.Schema, :binary) :: list(binary())
def relationships(module, :binary) do
module
|> relationships()
|> map(&to_string/1)
end
@spec relationship?(Ecto.Schema, atom() | binary()) :: boolean()
def relationship?(module, key) do
to_string(key) in relationships(module, :binary)
end
@spec source_schema(Ecto.Query) :: Ecto.Schema
def source_schema(query) do
query.from.source |> elem(1)
end
defp defines_schema?(module) do
Code.ensure_loaded(module)
function_exported?(module, :__schema__, 1)
end
defp assoc_schema(schema, assoc) when is_atom(assoc) do
case schema.__schema__(:association, assoc) do
%{related: related} ->
related
%{through: [through, child_assoc]} ->
through_schema = assoc_schema(schema, through)
assoc_schema(through_schema, child_assoc)
end
end
defp data_type(%Ecto.Association.Has{}=assoc, _) do
assoc_type = "has_#{assoc.cardinality}" |> String.to_atom
{assoc_type, assoc.related}
end
defp data_type(%Ecto.Association.HasThrough{}=assoc, schema) do
assoc_type = "has_#{assoc.cardinality}_through" |> String.to_atom
{assoc_type, assoc_schema(schema, assoc.field), assoc.through }
end
defp data_type(%Ecto.Association.BelongsTo{}=assoc, schema) do
{:belongs_to, assoc_schema(schema, assoc.field)}
end
defp data_type(%Ecto.Association.ManyToMany{}=assoc, schema) do
{:many_to_many, assoc_schema(schema, assoc.field), assoc.join_through, }
end
defp data_type(false, _), do: nil
defp data_type({:virtual, type}, _), do: {:virtual_field, type}
defp data_type(type, _), do: {:field, type}
end