defmodule Mix.PhoenixCustomGenerators do # Conveniences for Phoenix tasks. @moduledoc false @valid_attributes [:integer, :float, :decimal, :boolean, :map, :string, :array, :references, :text, :date, :time, :datetime, :uuid, :binary] @doc """ Copies files from source dir to target dir according to the given map. Files are evaluated against EEx according to the given binding. """ def copy_from(apps, source_dir, target_dir, binding, mapping) when is_list(mapping) do roots = Enum.map(apps, &to_app_source(&1, source_dir)) for {format, source_file_path, target_file_path} <- mapping do source = Enum.find_value(roots, fn root -> source = Path.join(root, source_file_path) if File.exists?(source), do: source end) || raise "could not find #{source_file_path} in any of the sources" target = Path.join(target_dir, target_file_path) contents = case format do :text -> File.read!(source) :eex -> EEx.eval_file(source, binding) end Mix.Generator.create_file(target, contents) 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) @doc """ Inflect path, scope, alias and more from the given name. iex> Mix.Phoenix.inflect("user") [alias: "User", human: "User", base: "Phoenix", module: "Phoenix.User", scoped: "User", singular: "user", path: "user"] iex> Mix.Phoenix.inflect("Admin.User") [alias: "User", human: "User", base: "Phoenix", module: "Phoenix.Admin.User", scoped: "Admin.User", singular: "user", path: "admin/user"] iex> Mix.Phoenix.inflect("Admin.SuperUser") [alias: "SuperUser", human: "Super user", base: "Phoenix", module: "Phoenix.Admin.SuperUser", scoped: "Admin.SuperUser", singular: "super_user", path: "admin/super_user"] """ def inflect(singular) do base = Mix.Phoenix.base scoped = Phoenix.Naming.camelize(singular) path = Phoenix.Naming.underscore(scoped) singular = String.split(path, "/") |> List.last module = Module.concat(base, scoped) |> inspect alias = String.split(module, ".") |> List.last human = Phoenix.Naming.humanize(singular) [alias: alias, human: human, base: base, module: module, scoped: scoped, singular: singular, path: path] end @doc """ Parses the attrs as received by generators. """ def attrs(attrs) do Enum.map(attrs, fn attr -> attr |> drop_unique() |> String.split(":", parts: 3) |> list_to_attr() |> validate_attr!() end) end @doc """ Fetches the unique attributes from attrs. """ def uniques(attrs) do attrs |> Enum.filter(&String.ends_with?(&1, ":unique")) |> Enum.map(& &1 |> String.split(":", parts: 2) |> hd |> String.to_atom) end @doc """ Generates some sample params based on the parsed attributes. """ def params(attrs, opts) do attrs |> Enum.reject(fn {_, {:references, _}} -> true {_, _} -> false end) |> Enum.into(%{}, fn {k, t} -> {k, type_to_default(t, opts)} end) end def params_factory_girl(attrs, opts) do attrs |> Enum.reject(fn {_, {:references, _}} -> true {_, _} -> false end) |> Enum.into(%{}, fn {k, t} -> {k, type_to_default_factory_girl(t, opts)} end) end def params_json(attrs, opts) do attrs |> Enum.reject(fn {_, {:references, _}} -> true {_, _} -> false end) |> Enum.into(%{}, fn {k, t} -> {k, type_to_default_json(t)} end) end @doc """ Checks the availability of a given module name. """ def check_module_name_availability!(name) do name = Module.concat(Elixir, name) if Code.ensure_loaded?(name) do Mix.raise "Module name #{inspect name} is already taken, please choose another name" end end @doc """ Returns the module base name based on the configuration value. config :my_app namespace: My.App """ def base do app = otp_app() case Application.get_env(app, :namespace, app) do ^app -> app |> to_string |> Phoenix.Naming.camelize mod -> mod |> inspect end end @doc """ Returns the otp app from the Mix project configuration. """ def otp_app do Mix.Project.config |> Keyword.fetch!(:app) end @doc """ Returns all compiled modules in a project. """ def modules do Mix.Project.compile_path |> Path.join("*.beam") |> Path.wildcard |> Enum.map(&beam_to_module/1) end defp beam_to_module(path) do path |> Path.basename(".beam") |> String.to_atom() end defp drop_unique(info) do prefix = byte_size(info) - 7 case info do <> -> attr _ -> info end end defp list_to_attr([key]), do: {String.to_atom(key), :string} defp list_to_attr([key, value]), do: {String.to_atom(key), String.to_atom(value)} defp list_to_attr([key, comp, value]) do {String.to_atom(key), {String.to_atom(comp), String.to_atom(value)}} end defp type_to_default(t, opts) do ecto_calendar_type = case Keyword.get(opts, :ecto_calendar_types, false) do true -> :ecto_calendar_type false -> :native end case {t, ecto_calendar_type} do {{:array, _}, _} -> [] {:integer, _} -> 42 {:float, _} -> "120.5" {:decimal, _} -> "120.5" {:boolean, _} -> true {:map, _} -> %{} {:text, _} -> "some content" {:date, :ecto_calendar_type} -> %{year: 2010, month: 4, day: 17} {:date, :native} -> %Date{year: 2010, month: 4, day: 17} {:time, :ecto_calendar_type} -> %{hour: 14, min: 0, sec: 0} {:time, :native} -> %Time{hour: 14, minute: 0, second: 0, microsecond: {0, 0}} {:datetime, :ecto_calendar_type} -> %{year: 2010, month: 4, day: 17, hour: 14, min: 0, sec: 0} {:datetime, :native} -> "2010-04-17 14:00:00.000000Z" {:uuid, _} -> "7488a646-e31f-11e4-aace-600308960662" _ -> "some content" end end defp type_to_default_factory_girl(t, opts) do ecto_calendar_type = case Keyword.get(opts, :ecto_calendar_types, false) do true -> :ecto_calendar_type false -> :native end case {t, ecto_calendar_type} do {{:array, _}, _} -> "[]" {:integer, _} -> "42" {:float, _} -> "120.5" {:decimal, _} -> "Decimal.new(\"120.5\")" {:boolean, _} -> "true" {:map, _} -> "%{}" {:text, _} -> "\"some content\"" {:date, :ecto_calendar_type} -> "%Ecto.Date{year: 2010, month: 4, day: 17}" {:date, :native} -> "%Date{year: 2010, month: 4, day: 17}" {:time, :ecto_calendar_type} -> "%Ecto.Time{hour: 14, min: 0, sec: 0}" {:time, :native} -> "%Time{hour: 14, minute: 0, second: 0, microsecond: {0, 0}}" {:datetime, :ecto_calendar_type} -> "%Ecto.DateTime{year: 2010, month: 4, day: 17, hour: 14, min: 0, sec: 0}" {:datetime, :native} -> "\"2010-04-17 14:00:00.000000Z\"" {:uuid, _} -> "\"7488a646-e31f-11e4-aace-600308960662\"" _ -> "\"some content\"" end end defp type_to_default_json(t) do case t do {:array, _} -> [] :integer -> 42 :float -> 120.5 :decimal -> "120.5" :boolean -> true :map -> %{} :text -> "some content" :date -> "2010-04-17" :time -> "14:00:00" :datetime -> "2010-04-17T14:00:00" :uuid -> "7488a646-e31f-11e4-aace-600308960662" _ -> "some content" end end defp validate_attr!({_name, type} = attr) when type in @valid_attributes, do: attr defp validate_attr!({_name, {type, _}} = attr) when type in @valid_attributes, do: attr defp validate_attr!({_, type}), do: Mix.raise "Unknown type `#{type}` given to generator" end