Current section
Files
Jump to
Current section
Files
lib/mix/phoenix_custom_generators.gen.model.ex
defmodule Mix.Tasks.PhoenixCustomGenerators.Gen.Model do
use Mix.Task
@shortdoc "Generates an Ecto model"
@moduledoc """
Generates an Ecto model in your Phoenix application.
mix phoenix.gen.model User users name:string age:integer
The first argument is the module name followed by its plural
name (used for the schema).
The generated model will contain:
* a schema file in web/models
* a migration file for the repository
The generated migration can be skipped with `--no-migration`.
## Attributes
The resource fields are given using `name:type` syntax
where type are the types supported by Ecto. Omitting
the type makes it default to `:string`:
mix phoenix.gen.model User users name age:integer
The generator also supports `belongs_to` associations
via references:
mix phoenix.gen.model Post posts title user_id:references:users
This will result in a migration with an `:integer` column
of `:user_id` and create an index. It will also generate
the appropriate `belongs_to` entry in the schema.
Furthermore an array type can also be given if it is
supported by your database, although it requires the
type of the underlying array element to be given too:
mix phoenix.gen.model User users nicknames:array:string
Unique columns can be automatically generated by using:
mix phoenix.gen.model Post posts title:unique unique_int:integer:unique
If no data type is given, it defaults to a string.
## Namespaced resources
Resources can be namespaced, for such, it is just necessary
to namespace the first argument of the generator:
mix phoenix.gen.model Admin.User users name:string age:integer
## binary_id
Generated migration can use `binary_id` for schema's primary key
and its references with option `--binary-id`.
This option assumes the project was generated with the `--binary-id`
option, that sets up models to use `binary_id` by default. If that's
not the case you can still set all your models to use `binary_id`
by default, by adding the following to your `model` function in
`web/web.ex` or before the `schema` declaration:
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
## Default options
This generator uses default options provided in the `:generators`
configuration of the `:phoenix` application. These are the defaults:
config :phoenix, :generators,
migration: true,
binary_id: false,
sample_binary_id: "11111111-1111-1111-1111-111111111111"
You can override those options per invocation by providing corresponding
switches, e.g. `--no-binary-id` to use normal ids despite the default
configuration or `--migration` to force generation of the migration.
"""
def run(args) do
switches = [
migration: :boolean,
binary_id: :boolean,
instructions: :string,
ex_machina: :string,
ecto_calendar_types: :boolean
]
{opts, parsed, _} = OptionParser.parse(args, switches: switches)
[singular, plural | attrs] = validate_args!(parsed)
default_opts = Application.get_env(:phoenix, :generators, [])
opts = Keyword.merge(default_opts, opts)
uniques = Mix.PhoenixCustomGenerators.uniques(attrs)
attrs = Mix.PhoenixCustomGenerators.attrs(attrs)
binding = Mix.PhoenixCustomGenerators.inflect(singular)
params = Mix.PhoenixCustomGenerators.params(attrs, opts)
path = binding[:path]
Mix.PhoenixCustomGenerators.check_module_name_availability!(binding[:module])
{assocs, attrs} = partition_attrs_and_assocs(attrs)
binding = binding ++
[
attrs: attrs,
plural: plural,
types: types(attrs, opts),
uniques: uniques,
assocs: assocs(assocs),
indexes: indexes(plural, assocs, uniques),
schema_defaults: schema_defaults(attrs),
binary_id: opts[:binary_id],
migration_defaults: migration_defaults(attrs),
params: params,
params_factory_girl: Mix.PhoenixCustomGenerators.params_factory_girl(attrs, opts),
]
files = [
{:eex, "model.ex", "web/models/#{path}.ex"},
{:eex, "model_test.exs", "test/models/#{path}_test.exs"},
] ++ migration(opts[:migration], path)
Mix.PhoenixCustomGenerators.copy_from paths(), "priv/templates/phoenix_custom_generators.gen.model", "", binding, files
# Print any extra instruction given by parent generators
Mix.shell.info opts[:instructions] || ""
if opts[:migration] != false do
Mix.shell.info """
Remember to update your repository by running migrations:
$ mix ecto.migrate
"""
end
if opts[:ex_machina_module] do
possible_files = Enum.map(paths(), &to_app_source(&1, "priv/templates/phoenix_custom_generators.gen.model/ex_machina.exs"))
source = Enum.find_value(possible_files, fn possible_file ->
if File.exists?(possible_file), do: possible_file
end) || raise "could not find ex_machina factory template file"
Mix.shell.info """
Add the following function to your ex_machina factory module:
#{EEx.eval_file(source, binding)}
"""
end
end
defp to_app_source(path, source_dir) when is_binary(path),
do: Path.join(path, source_dir)
defp to_app_source(app, source_dir) when is_atom(app),
do: Application.app_dir(app, source_dir)
defp validate_args!([_, plural | _] = args) do
cond do
String.contains?(plural, ":") ->
raise_with_help()
plural != Phoenix.Naming.underscore(plural) ->
Mix.raise "Expected the second argument, #{inspect plural}, to be all lowercase using snake_case convention"
true ->
args
end
end
defp validate_args!(_) do
raise_with_help()
end
@spec raise_with_help() :: no_return()
defp raise_with_help do
Mix.raise """
mix phoenix.gen.model expects both singular and plural names
of the generated resource followed by any number of attributes:
mix phoenix.gen.model User users name:string
"""
end
defp partition_attrs_and_assocs(attrs) do
Enum.partition attrs, fn
{_, {:references, _}} ->
true
{key, :references} ->
Mix.raise """
Phoenix generators expect the table to be given to #{key}:references.
For example:
mix phoenix.gen.model Comment comments body:text post_id:references:posts
"""
_ ->
false
end
end
defp assocs(assocs) do
Enum.map assocs, fn {key_id, {:references, source}} ->
key = String.replace(Atom.to_string(key_id), "_id", "")
assoc = Mix.PhoenixCustomGenerators.inflect key
{String.to_atom(key), key_id, assoc[:module], source}
end
end
defp indexes(plural, assocs, uniques) do
Enum.concat(
Enum.map(uniques, fn key -> {key, true} end),
Enum.map(assocs, fn {key, _} -> {key, false} end))
|> Enum.uniq_by(fn {key, _} -> key end)
|> Enum.map(fn
{key, false} -> "create index(:#{plural}, [:#{key}])"
{key, true} -> "create unique_index(:#{plural}, [:#{key}])"
end)
end
defp migration(false, _path), do: []
defp migration(_, path) do
[{:eex, "migration.exs",
"priv/repo/migrations/#{timestamp()}_create_#{String.replace(path, "/", "_")}.exs"}]
end
defp timestamp do
{{y, m, d}, {hh, mm, ss}} = :calendar.universal_time()
"#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}"
end
defp pad(i) when i < 10, do: << ?0, ?0 + i >>
defp pad(i), do: to_string(i)
defp types(attrs, opts) do
Enum.into attrs, %{}, fn
{k, {c, v}} -> {k, {c, value_to_type(v, opts)}}
{k, v} -> {k, value_to_type(v, opts)}
end
end
defp schema_defaults(attrs) do
Enum.into attrs, %{}, fn
{k, :boolean} -> {k, ", default: false"}
{k, _} -> {k, ""}
end
end
defp migration_defaults(attrs) do
Enum.into attrs, %{}, fn
{k, :boolean} -> {k, ", default: false, null: false"}
{k, _} -> {k, ""}
end
end
defp value_to_type(:text, _opts), do: :string
defp value_to_type(:uuid, _opts), do: Ecto.UUID
defp value_to_type(:date, opts) do
if Keyword.get(opts, :ecto_calendar_types, false) do
Ecto.Date
else
:date
end
end
defp value_to_type(:time, opts) do
if Keyword.get(opts, :ecto_calendar_types, false) do
Ecto.Time
else
:time
end
end
defp value_to_type(:datetime, opts) do
if Keyword.get(opts, :ecto_calendar_types, false) do
Ecto.DateTime
else
:utc_datetime
end
end
defp value_to_type(v, _opts) do
if Code.ensure_loaded?(Ecto.Type) and not Ecto.Type.primitive?(v) do
Mix.raise "Unknown type `#{v}` given to generator"
else
v
end
end
defp paths do
[".", :phoenix_custom_generators]
end
end