defmodule Mix.Tasks.Mandarin.Gen.Schema do @shortdoc "Generates an Ecto schema and migration file" @moduledoc """ Generates an Ecto schema and migration. mix mandarin.gen.schema Blog.Post blog_posts title:string views:integer The first argument is the schema module followed by its plural name (used as the table name). The generated schema above will contain: * a schema file in `lib/my_app/blog/post.ex`, with a `blog_posts` table * a migration file for the repository The generated migration can be skipped with `--no-migration`. ## Contexts Your schemas can be generated and added to a separate OTP app. Make sure your configuration is properly setup or manually specify the context app with the `--context-app` option with the CLI. # Via config config :marketing_web, :generators, context_app: :marketing # Via CLI mix mandarin.gen.schema Blog.Post blog_posts title:string views:integer --context-app marketing ## 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 mandarin.gen.schema Blog.Post blog_posts title views:integer The following types are supported: #{for attr <- Mix.Mandarin.Schema.valid_types(), do: " * `#{inspect(attr)}`\n"} * `:datetime` - An alias for `:naive_datetime` The generator also supports references, which we will properly associate the given column to the primary key column of the referenced table: mix mandarin.gen.schema Blog.Post blog_posts title user_id:references:users This will result in a migration with an `:integer` column of `:user_id` and create an index. 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 mandarin.gen.schema Blog.Post blog_posts tags:array:string Unique columns can be automatically generated by using: mix mandarin.gen.schema Blog.Post blog_posts title:unique unique_int:integer:unique If no data type is given, it defaults to a string. ## table By default, the table name for the migration and schema will be the plural name provided for the resource. To customize this value, a `--table` option may be provided. For example: mix mandarin.gen.schema Blog.Post posts --table cms_posts ## binary_id Generated migration can use `binary_id` for schema's primary key and its references with option `--binary-id`. ## Default options This generator uses default options provided in the `:generators` configuration of your application. These are the defaults: config :your_app, :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. """ use Mix.Task alias Mix.Mandarin.Schema @switches [ migration: :boolean, binary_id: :boolean, table: :string, web: :string, context_app: :string ] @doc false def run(args) do if Mix.Project.umbrella?() do Mix.raise("mix mandarin.gen.schema can only be run inside an application directory") end schema = build(args, []) paths = Mix.Mandarin.generator_paths() prompt_for_conflicts(schema) schema |> copy_new_files(paths, schema: schema) |> print_shell_instructions() end defp prompt_for_conflicts(schema) do schema |> files_to_be_generated() |> Mix.Mandarin.prompt_for_conflicts() end @doc false def build(args, parent_opts, help \\ __MODULE__) do {schema_opts, parsed, _} = OptionParser.parse(args, switches: @switches) [schema_name, plural | attrs] = validate_args!(parsed, help) opts = parent_opts |> Keyword.merge(schema_opts) |> put_context_app(schema_opts[:context_app]) schema = Schema.new(schema_name, plural, attrs, opts) schema end defp put_context_app(opts, nil), do: opts defp put_context_app(opts, string) do Keyword.put(opts, :context_app, String.to_atom(string)) end @doc false def files_to_be_generated(%Schema{} = schema) do [{:eex, "schema.ex", schema.file}] end @doc false def copy_new_files(%Schema{context_app: ctx_app} = schema, paths, binding) do files = files_to_be_generated(schema) Mix.Mandarin.copy_from(paths, "priv/templates/mandarin.gen.schema", binding, files) if schema.migration? do migration_path = Mix.Mandarin.context_app_path( ctx_app, "priv/repo/migrations/#{timestamp()}_create_#{schema.table}.exs" ) Mix.Mandarin.copy_from(paths, "priv/templates/mandarin.gen.schema", binding, [ {:eex, "migration.exs", migration_path} ]) end schema end @doc false def print_shell_instructions(%Schema{} = schema) do if schema.migration? do Mix.shell().info(""" Remember to update your repository by running migrations: $ mix ecto.migrate """) end end @doc false def validate_args!([schema, plural | _] = args, help) do cond do not Schema.valid?(schema) -> help.raise_with_help( "Expected the schema argument, #{inspect(schema)}, to be a valid module name" ) String.contains?(plural, ":") or plural != Mandarin.Naming.underscore(plural) -> help.raise_with_help( "Expected the plural argument, #{inspect(plural)}, to be all lowercase using snake_case convention" ) true -> args end end def validate_args!(_, help) do help.raise_with_help("Invalid arguments") end @doc false @spec raise_with_help(String.t()) :: no_return() def raise_with_help(msg) do Mix.raise(""" #{msg} mix mandarin.gen.schema expects both a module name and the plural of the generated resource followed by any number of attributes: mix mandarin.gen.schema Blog.Post blog_posts title:string """) 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: <> defp pad(i), do: to_string(i) end