defmodule Inspecto do @moduledoc """ `Inspecto` is a utility for inspecting Ecto schemas to view their field names, data types, and default values. Ecto schema modules do not contain full information about your database schemas: they only contain enough information to act as a viable intermediary for the Elixir layer. You cannot, for example, know character length limits or input constraints by merely inspecting Ecto schemas. Although Ecto _migrations_ contain a lot more of this information, they too aren't great for the purpose because migrations are additive with changes spread out over time, and importantly, there's not requirement that a database be defined via migrations. ## Usage The expected usage of this package is to call `Inspecto.summarize/2` from within a `@moduledoc` tag somewhere inside your app, wherever you wish a summary of your Ecto schemas to appear, e.g. something like this: ``` defmodule MyApp.MyModel do @moduledoc \"\"\" Here is a summary of my Ecto schemas: \#\{ MyApp.MyModel |> Inspecto.modules() |> Inspecto.summarize(format: :html)\} \"\"\" end ``` This will render a series of HTML tables at compile-time. > #### Warning {: .warning} > > When you call a function from inside a `@moduledoc` tag, it is evaluated > at *compile-time*. `Inspecto` _should_ filter out problems and avoid raising > errors, but if it generates a problem for any reason, be aware that you may > need to remove any calls to its functions from your `@moduledoc` tags. """ alias Inspecto.Schema require EEx @local_path Application.app_dir(:inspecto, ["priv/resources"]) @doc """ Summarizes the given Ecto schema modules using the format provided. It is necessary to supply a list of modules. ## Options - `:format` controls the format of the output. Supported values: `:html`, `:raw` (default). - `:h` controls the heading level used for each schema (`3` corresponds to an `h3` tag). This is only relevant when `:format` is set to `:html`. Default: `2` The `:raw` format returns information as a list of `Inspecto.Schema` structs. The `:html` format returns information as an HTML table. This is suitable for use inside a `@moduledoc` attribute. Other formats may be supported in the future (e.g. Mermaid JS, but it's not yet compatible with how documentation generation strips out newlines). ## Examples iex> MyApp.Schemas |> Inspecto.modules() |> Inspecto.summarize(format: :raw) [ %Inspecto.Schema{ fields: [ %Inspecto.Schema.Field{default: nil, name: :id, type: :binary_id}, %Inspecto.Schema.Field{default: nil, name: :meta, type: :map}, %Inspecto.Schema.Field{default: 30, name: :net, type: :integer}, # ... ], module:MyApp.Schemas.SomeSchema, primary_key: [:id], source: "some_table" } ], # ... """ @spec summarize(modules :: [module()], opts :: Keyword.t()) :: String.t() | [Schema.t()] def summarize(modules, opts \\ []) when is_list(modules) do format = Keyword.get(opts, :format, :raw) heading_level = Keyword.get(opts, :h, 2) modules |> Enum.reduce([], fn m, acc -> case Schema.inspect(m) do {:ok, schema} -> [schema | acc] {:invalid_module, _} -> acc end end) |> Enum.reverse() |> apply_format(format, heading_level) end defp apply_format(schemas, :raw, _), do: schemas defp apply_format(schemas, :html, heading_level), do: table_wrapper(schemas, heading_level) EEx.function_from_file( :defp, :table_wrapper, "#{@local_path}/table_wrapper.eex", [:schemas, :heading_level] ) EEx.function_from_file(:defp, :table, "#{@local_path}/table.eex", [:schema, :heading_level]) defp stringify(module), do: String.trim_leading("#{module}", "Elixir.") @doc """ This is a convenience function which retrieves a list of modules in the given "namespace". This is a useful way of gathering up modules that define Ecto schemas. ## Examples iex> Inspecto.modules(MyApp) [MyApp.Foo, MyApp.Bar, ...] """ def modules(namespace) when is_atom(namespace) do :code.all_available() |> Enum.filter(fn {name, _file, _loaded} -> String.starts_with?(to_string(name), to_string(namespace)) end) |> Enum.map(fn {name, _file, _loaded} -> name |> List.to_atom() end) end end