mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 21:13:10 +12:00
improvement: initial implementation of ash resource formatter
This commit is contained in:
parent
9eff65758c
commit
4138bd4934
4 changed files with 240 additions and 0 deletions
|
@ -137,6 +137,7 @@ locals_without_parens = [
|
|||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
||||
locals_without_parens: locals_without_parens,
|
||||
plugins: [Ash.ResourceFormatter],
|
||||
export: [
|
||||
locals_without_parens: locals_without_parens
|
||||
]
|
||||
|
|
234
lib/ash/resource_formatter.ex
Normal file
234
lib/ash/resource_formatter.ex
Normal file
|
@ -0,0 +1,234 @@
|
|||
defmodule Ash.ResourceFormatter do
|
||||
@moduledoc """
|
||||
Formats Ash resources. WARNING: This is untested, use at your own risk! *do not run without commiting your code first*!
|
||||
|
||||
Currently, it is very simple, and will only reorder the outermost sections according to some rules.
|
||||
|
||||
# Plugin
|
||||
|
||||
Include the plugin into your `.formatter.exs` like so `plugins: [Ash.ResourceFormatter]`.
|
||||
|
||||
If no configuration is provided, it will sort all top level DSL sections *alphabetically*.
|
||||
|
||||
# Section Order
|
||||
|
||||
To provide a custom section order (for both ash and any extensions), add configuration to your app, for example:
|
||||
|
||||
```elixir
|
||||
config :ash, :formatter,
|
||||
section_order: [
|
||||
:resource,
|
||||
:postgres,
|
||||
:attributes,
|
||||
:relationships,
|
||||
:aggregates,
|
||||
:calculations
|
||||
]
|
||||
```
|
||||
|
||||
Any sections found in your resource that aren't in that list will be left in the order that they were in, the sections
|
||||
in the list will be sorted "around" those sections. E.g the following list: `[:code_interface, :attributes]` can be interpreted as
|
||||
"ensure that code_interface comes before attributes, and don't change the rest".
|
||||
|
||||
# Using something other than `Ash.Resource`
|
||||
|
||||
The resource formatter triggers when it sees a `use Ash.Resource` in a module. In some cases, you may be
|
||||
"use"ing a different module, e.g `use MyApp.Resource`. To support this, you can configure the `using_modules`, like so:
|
||||
|
||||
```elixir
|
||||
config :ash, :formatter,
|
||||
using_modules: [Ash.Resource, MyApp.Resource]
|
||||
```
|
||||
"""
|
||||
@behaviour Mix.Tasks.Format
|
||||
|
||||
def features(_opts) do
|
||||
[extensions: [".ex", ".exs"]]
|
||||
end
|
||||
|
||||
def format(contents, opts) do
|
||||
IO.inspect(opts)
|
||||
using_modules = Application.get_env(:ash, :formatter)[:using_modules] || [Ash.Resource]
|
||||
|
||||
contents
|
||||
|> Sourceror.parse_string!()
|
||||
|> format_resources(opts, using_modules)
|
||||
|> IO.inspect()
|
||||
|> then(fn patches ->
|
||||
Sourceror.patch_string(contents, patches)
|
||||
end)
|
||||
|> Code.format_string!(opts_without_plugin(opts))
|
||||
|> then(fn iodata ->
|
||||
[iodata, ?\n]
|
||||
end)
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
# rescue
|
||||
# e ->
|
||||
# IO.inspect(e)
|
||||
# contents
|
||||
end
|
||||
|
||||
defp format_resources(parsed, opts, using_modules) do
|
||||
{_, patches} =
|
||||
Macro.prewalk(parsed, [], fn
|
||||
{:defmodule, _, [_, [{{:__block__, _, [:do]}, {:__block__, _, body}}]]} = expr, patches ->
|
||||
case get_extensions(body, using_modules) do
|
||||
{:ok, extensions} ->
|
||||
IO.inspect(extensions, label: "extensions")
|
||||
replacement = format_resource(body, extensions)
|
||||
|
||||
patches =
|
||||
body
|
||||
|> Enum.zip(replacement)
|
||||
|> Enum.reduce(patches, fn {body_section, replacement_section}, patches ->
|
||||
if body_section == replacement_section do
|
||||
patches
|
||||
else
|
||||
[
|
||||
%{
|
||||
range: Sourceror.get_range(body_section, include_comments: true),
|
||||
change: Sourceror.to_string(replacement_section, opts)
|
||||
}
|
||||
| patches
|
||||
]
|
||||
end
|
||||
end)
|
||||
|
||||
{expr, patches}
|
||||
|
||||
_ ->
|
||||
{expr, patches}
|
||||
end
|
||||
|
||||
expr, patches ->
|
||||
{expr, patches}
|
||||
end)
|
||||
|
||||
patches
|
||||
end
|
||||
|
||||
defp format_resource(body, extensions) do
|
||||
sections =
|
||||
[Ash.Resource.Dsl | extensions]
|
||||
|> Enum.flat_map(fn extension ->
|
||||
Enum.map(extension.sections(), fn section ->
|
||||
{extension, section}
|
||||
end)
|
||||
end)
|
||||
|> sort_sections()
|
||||
|
||||
section_names = Enum.map(sections, fn {_, section} -> section.name end)
|
||||
|
||||
{section_exprs, non_section_exprs} =
|
||||
body
|
||||
|> Enum.split_with(fn {name, _, _} ->
|
||||
name in section_names
|
||||
end)
|
||||
|
||||
new_sections =
|
||||
section_names
|
||||
|> Enum.flat_map(fn section_name ->
|
||||
matching_section =
|
||||
Enum.find(section_exprs, fn
|
||||
{^section_name, _, _} -> true
|
||||
_ -> nil
|
||||
end)
|
||||
|
||||
case matching_section do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
section_expr ->
|
||||
[section_expr]
|
||||
end
|
||||
end)
|
||||
|
||||
Enum.concat(non_section_exprs, new_sections)
|
||||
end
|
||||
|
||||
defp sort_sections(sections) do
|
||||
case Application.get_env(:ash, :formatter)[:section_order] do
|
||||
nil ->
|
||||
Enum.sort_by(sections, fn {_extension, section} ->
|
||||
section.name
|
||||
end)
|
||||
|
||||
section_order ->
|
||||
{ordered, unordered} =
|
||||
sections
|
||||
|> Enum.with_index()
|
||||
|> Enum.split_with(fn {{_, section}, _} ->
|
||||
section.name in section_order
|
||||
end)
|
||||
|
||||
reordered =
|
||||
ordered
|
||||
|> Enum.map(&elem(&1, 0))
|
||||
|> Enum.sort_by(fn {_, section} ->
|
||||
Enum.find_index(section_order, &(&1 == section.name))
|
||||
end)
|
||||
|
||||
Enum.reduce(unordered, reordered, fn {{extension, section}, i}, acc ->
|
||||
List.insert_at(acc, i, {extension, section})
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_extensions(body, using_modules) do
|
||||
Enum.find_value(body, :error, fn
|
||||
{:use, _, using} ->
|
||||
[using, opts] =
|
||||
case Ash.Dsl.Extension.expand_alias(using, __ENV__) do
|
||||
[using] ->
|
||||
[using, []]
|
||||
|
||||
[using, opts] ->
|
||||
[using, opts]
|
||||
end
|
||||
|> IO.inspect()
|
||||
|
||||
if using in using_modules do
|
||||
{:ok, parse_extensions(opts)}
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end)
|
||||
end
|
||||
|
||||
defp parse_extensions(blocks) do
|
||||
blocks
|
||||
|> Enum.flat_map(fn {{:__block__, _, _}, extensions} ->
|
||||
extensions
|
||||
|> case do
|
||||
{:__block__, _, [extensions]} ->
|
||||
extensions
|
||||
|
||||
extension when is_atom(extension) ->
|
||||
extension
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
|> List.wrap()
|
||||
|> Enum.flat_map(fn extension ->
|
||||
case Code.ensure_compiled(extension) do
|
||||
{:module, module} ->
|
||||
if Ash.Helpers.implements_behaviour?(module, Ash.Dsl.Extension) do
|
||||
[module]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp opts_without_plugin(opts) do
|
||||
Keyword.update(opts, :plugins, [], &(&1 -- [__MODULE__]))
|
||||
end
|
||||
end
|
4
mix.exs
4
mix.exs
|
@ -111,6 +111,9 @@ defmodule Ash.MixProject do
|
|||
Ash.Filter,
|
||||
Ash.Sort
|
||||
],
|
||||
formatting: [
|
||||
Ash.ResourceFormatter
|
||||
],
|
||||
validations: ~r/Ash.Resource.Validation/,
|
||||
changes: ~r/Ash.Resource.Change/,
|
||||
calculations: [
|
||||
|
@ -199,6 +202,7 @@ defmodule Ash.MixProject do
|
|||
{:timex, ">= 3.0.0"},
|
||||
{:comparable, "~> 1.0"},
|
||||
{:jason, ">= 1.0.0"},
|
||||
{:sourceror, "~> 0.9"},
|
||||
# Dev/Test dependencies
|
||||
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
|
||||
{:ex_check, "~> 0.12.0", only: :dev},
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -36,6 +36,7 @@
|
|||
"picosat_elixir": {:hex, :picosat_elixir, "0.2.0", "1c9d2d65b32039c9e3eb600bff903579e5916f559dbf0013b3c4ca617c93ac64", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7900c6d58d65a9d8a2899f53019fb70e9b9678161bbb53f646e28e146aca138c"},
|
||||
"providers": {:hex, :providers, "1.8.1", "70b4197869514344a8a60e2b2a4ef41ca03def43cfb1712ecf076a0f3c62f083", [:rebar3], [{:getopt, "1.0.1", [hex: :getopt, repo: "hexpm", optional: false]}], "hexpm", "e45745ade9c476a9a469ea0840e418ab19360dc44f01a233304e118a44486ba0"},
|
||||
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
|
||||
"sourceror": {:hex, :sourceror, "0.9.0", "77e8f883be9455812d15913582d2985048ef65d7d931072c548e025a6ea58d5a", [:mix], [], "hexpm", "f56fb5b935df7784504f7d1ba074e0aa83299e2ebd64f75268ffcae62a28f331"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||
"telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
|
||||
"timex": {:hex, :timex, "3.7.5", "3eca56e23bfa4e0848f0b0a29a92fa20af251a975116c6d504966e8a90516dfd", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "a15608dca680f2ef663d71c95842c67f0af08a0f3b1d00e17bbd22872e2874e4"},
|
||||
|
|
Loading…
Reference in a new issue