feat: extension section module imports, generated .formatter.exs (#71)

This commit is contained in:
Zach Daniel 2020-06-15 02:40:33 -04:00 committed by GitHub
parent c776da9237
commit b6101a4dff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 290 additions and 77 deletions

View file

@ -17,7 +17,8 @@
"mix coveralls.github"
else
"mix test"
end}
end},
{:check_formatter, command: "mix ash.formatter --check"}
## custom new tools may be added (mix tasks or arbitrary commands)
# {:my_mix_task, command: "mix release", env: %{"MIX_ENV" => "prod"}},

View file

@ -1,27 +1,47 @@
# Used by "mix format"
# THIS FILE IS AUTOGENERATED USING `mix ash.formatter`
# DONT MODIFY IT BY HAND
locals_without_parens = [
read: 1,
read: 2,
create: 1,
create: 2,
update: 1,
update: 2,
destroy: 1,
destroy: 2,
actions: 1,
allow_nil?: 1,
attribute: 2,
attribute: 3,
belongs_to: 2,
belongs_to: 3,
has_one: 2,
has_one: 3,
create: 1,
create: 2,
create_timestamp: 1,
create_timestamp: 2,
default: 1,
define_field?: 1,
description: 1,
destination_field: 1,
destination_field_on_join_table: 1,
destroy: 1,
destroy: 2,
field_type: 1,
generated?: 1,
has_many: 2,
has_many: 3,
has_one: 2,
has_one: 3,
many_to_many: 2,
many_to_many: 3,
resources: 1,
destination_field: 1,
allow_nil?: 1
primary?: 1,
primary_key?: 1,
read: 1,
read: 2,
resource: 1,
resource: 2,
reverse_relationship: 1,
source_field: 1,
source_field_on_join_table: 1,
through: 1,
type: 1,
update: 1,
update: 2,
update_default: 1,
update_timestamp: 1,
update_timestamp: 2,
writable?: 1
]
[

View file

@ -1,4 +1,4 @@
name: Elixir CI
name: Ash CI
on:
push:

View file

@ -106,12 +106,19 @@ defmodule Ash do
|> Enum.find(&(&1.name == relationship_name))
end
@spec resource_module?(module) :: boolean
def resource_module?(module) do
def implements_behaviour?(module, behaviour) do
:attributes
|> module.module_info()
|> Keyword.get(:behaviour, [])
|> Enum.any?(&(&1 == Ash.Resource))
|> Enum.flat_map(fn
{:behaviour, value} -> List.wrap(value)
_ -> []
end)
|> Enum.any?(&(&1 == behaviour))
end
@spec resource_module?(module) :: boolean
def resource_module?(module) do
implements_behaviour?(module, Ash.Resource)
end
@doc false

View file

@ -27,13 +27,17 @@ defmodule Ash.Dsl.Entity do
other values in that struct. If you need things that aren't contained in that struct, use an
`Ash.Dsl.Transformer`.
`entities` allows you to specify a keyword list of nested entities. Nested entities are stored
on the struct in the corresponding key, and are used in the same way entities are otherwise.
For a full example, see `Ash.Dsl.Extension`.
"""
defstruct [
:name,
:examples,
:target,
:transform,
examples: [],
entities: [],
describe: "",
args: [],
schema: [],
@ -47,17 +51,20 @@ defmodule Ash.Dsl.Entity do
examples: [String.t()],
transform: mfa | nil,
args: [atom],
entities: Keyword.t(),
auto_set_fields: Keyword.t(),
schema: NimbleOptions.schema()
}
def build(
%{target: target, schema: schema, auto_set_fields: auto_set_fields, transform: transform},
opts
opts,
nested_entities
) do
with {:ok, opts} <- NimbleOptions.validate(opts, schema),
opts <- Keyword.merge(opts, auto_set_fields || []),
built <- struct(target, opts),
built <- struct(built, nested_entities),
{:ok, built} <-
transform(transform, built) do
{:ok, built}

View file

@ -149,7 +149,7 @@ defmodule Ash.Dsl.Extension do
|> Enum.filter(fn key ->
is_tuple(key) and elem(key, 0) == __MODULE__
end)
|> Enum.each(&Process.delete/1)
|> Enum.each(&:persistent_term.delete/1)
end
imports =
@ -351,6 +351,13 @@ defmodule Ash.Dsl.Extension do
section_path = unquote(path ++ [section.name])
section = unquote(Macro.escape(section))
configured_imports =
for module <- unquote(section.imports) do
quote do
import unquote(module)
end
end
entity_imports =
for module <- unquote(entity_modules) do
quote do
@ -376,6 +383,13 @@ defmodule Ash.Dsl.Extension do
]
end
configured_unimports =
for module <- unquote(section.imports) do
quote do
import unquote(module), only: []
end
end
entity_unimports =
for module <- unquote(entity_modules) do
quote do
@ -404,6 +418,7 @@ defmodule Ash.Dsl.Extension do
entity_imports ++
section_imports ++
opts_import ++
configured_imports ++
[
quote do
unquote(body[:do])
@ -428,12 +443,15 @@ defmodule Ash.Dsl.Extension do
path: unquote(section_path)
end
Process.put({__MODULE__, :ash, unquote(section_path)}, %{
entities: current_config.entities,
opts: opts
})
Process.put(
{__MODULE__, :ash, unquote(section_path)},
%{
entities: current_config.entities,
opts: opts
}
)
end
] ++ opts_unimport ++ entity_unimports ++ section_unimports
] ++ configured_unimports ++ opts_unimport ++ entity_unimports ++ section_unimports
end
end
end
@ -496,6 +514,7 @@ defmodule Ash.Dsl.Extension do
section_path = unquote(Macro.escape(section_path))
field = unquote(Macro.escape(field))
extension = unquote(extension)
section = unquote(Macro.escape(section))
quote do
current_sections = Process.get({__MODULE__, :ash_sections}, [])
@ -515,8 +534,8 @@ defmodule Ash.Dsl.Extension do
Process.put(
{__MODULE__, :ash, unquote(section_path)},
%{
entities: current_config.entities,
opts: Keyword.put(current_config.opts, unquote(field), unquote(value))
current_config
| opts: Keyword.put(current_config.opts, unquote(field), unquote(value))
}
)
end
@ -533,12 +552,32 @@ defmodule Ash.Dsl.Extension do
end
@doc false
def build_entity(mod, extension, section_path, entity) do
mod_name = Module.concat(mod, Macro.camelize(to_string(entity.name)))
def build_entity(mod, extension, section_path, entity, nested_entity_path \\ []) do
nested_entity_parts = Enum.map(nested_entity_path, &Macro.camelize(to_string(&1)))
options_mod_name = Module.concat([mod, Macro.camelize(to_string(entity.name)), "Options"])
mod_parts =
Enum.concat([[mod], nested_entity_parts, [Macro.camelize(to_string(entity.name))]])
Ash.Dsl.Extension.build_entity_options(options_mod_name, entity.schema)
mod_name = Module.concat(mod_parts)
options_mod_name = Module.concat(mod_name, "Options")
nested_entity_mods =
Enum.flat_map(entity.entities, fn {key, entities} ->
entities
|> List.wrap()
|> Enum.map(fn entity ->
build_entity(
mod_name,
extension,
section_path,
entity,
nested_entity_path ++ [key]
)
end)
end)
Ash.Dsl.Extension.build_entity_options(options_mod_name, entity.schema, nested_entity_path)
args = Enum.map(entity.args, &Macro.var(&1, mod_name))
@ -549,65 +588,87 @@ defmodule Ash.Dsl.Extension do
entity: Macro.escape(entity),
args: Macro.escape(args),
section_path: Macro.escape(section_path),
options_mod_name: Macro.escape(options_mod_name)
options_mod_name: Macro.escape(options_mod_name),
nested_entity_mods: Macro.escape(nested_entity_mods),
nested_entity_path: Macro.escape(nested_entity_path)
] do
@doc Ash.Dsl.Entity.describe(entity)
defmacro unquote(entity.name)(unquote_splicing(args), opts \\ []) do
section_path = unquote(section_path)
section_path = unquote(Macro.escape(section_path))
entity_schema = unquote(Macro.escape(entity.schema))
entity = unquote(Macro.escape(entity))
entity_name = unquote(entity.name)
entity_args = unquote(entity.args)
options_mod_name = unquote(options_mod_name)
entity_name = unquote(Macro.escape(entity.name))
entity_args = unquote(Macro.escape(entity.args))
options_mod_name = unquote(Macro.escape(options_mod_name))
source = unquote(__MODULE__)
extension = unquote(extension)
extension = unquote(Macro.escape(extension))
nested_entity_mods = unquote(Macro.escape(nested_entity_mods))
nested_entity_path = unquote(Macro.escape(nested_entity_path))
arg_values = unquote(args)
quote do
alias Ash.Dsl.Entity
section_path = unquote(section_path)
entity_name = unquote(entity_name)
extension = unquote(extension)
current_config =
Process.get(
{__MODULE__, :ash, section_path},
%{entities: [], opts: []}
)
current_sections = Process.get({__MODULE__, :ash_sections}, [])
opts_without_do =
Process.put(
{:builder_opts, unquote(nested_entity_path)},
Keyword.merge(
unquote(Keyword.delete(opts, :do)),
Enum.zip(unquote(entity_args), unquote(arg_values))
)
)
import unquote(options_mod_name)
Ash.Dsl.Extension.import_mods(unquote(nested_entity_mods))
unquote(opts[:do])
current_config =
Process.get(
{__MODULE__, :ash, section_path ++ unquote(nested_entity_path)},
%{entities: [], opts: []}
)
import unquote(options_mod_name), only: []
all_opts =
case Process.delete(:builder_opts) do
nil ->
opts_without_do
Ash.Dsl.Extension.unimport_mods(unquote(nested_entity_mods))
opts ->
Keyword.merge(opts, opts_without_do)
end
opts = Process.delete({:builder_opts, unquote(nested_entity_path)})
alias Ash.Dsl.Entity
nested_entities =
unquote(Macro.escape(entity.entities))
|> Enum.map(&elem(&1, 0))
|> Enum.uniq()
|> Enum.reduce(%{}, fn key, acc ->
nested_path = section_path ++ unquote(nested_entity_path) ++ [key]
entities =
{__MODULE__, :ash, nested_path}
|> Process.get(%{entities: []})
|> Map.get(:entities, [])
|> Enum.reverse()
Map.update(acc, key, entities, fn current_nested_entities ->
(current_nested_entities || []) ++ entities
end)
end)
built =
case Entity.build(unquote(Macro.escape(entity)), all_opts) do
case Entity.build(unquote(Macro.escape(entity)), opts, nested_entities) do
{:ok, built} ->
Map.put(built, :__entity_name__, unquote(entity_name))
built
{:error, error} ->
additional_path =
if all_opts[:name] do
[unquote(entity.name), all_opts[:name]]
if opts[:name] do
[unquote(entity.name), opts[:name]]
else
[unquote(entity.name)]
end
@ -629,7 +690,7 @@ defmodule Ash.Dsl.Extension do
path: section_path ++ additional_path
end
new_config = %{opts: current_config.opts, entities: [built | current_config.entities]}
new_config = %{current_config | entities: current_config.entities ++ [built]}
unless {extension, section_path} in current_sections do
Process.put({__MODULE__, :ash_sections}, [
@ -638,7 +699,7 @@ defmodule Ash.Dsl.Extension do
end
Process.put(
{__MODULE__, :ash, section_path},
{__MODULE__, :ash, section_path ++ unquote(nested_entity_path)},
new_config
)
end
@ -650,20 +711,41 @@ defmodule Ash.Dsl.Extension do
mod_name
end
defmacro import_mods(mods) do
for mod <- mods do
quote do
import unquote(mod)
end
end
end
defmacro unimport_mods(mods) do
for mod <- mods do
quote do
import unquote(mod), only: []
end
end
end
@doc false
def build_entity_options(module_name, schema) do
def build_entity_options(module_name, schema, nested_entity_path) do
Module.create(
module_name,
quote bind_quoted: [schema: Macro.escape(schema)] do
quote bind_quoted: [schema: Macro.escape(schema), nested_entity_path: nested_entity_path] do
@moduledoc false
for {key, value} <- schema do
for {key, _value} <- schema do
defmacro unquote(key)(value) do
key = unquote(key)
nested_entity_path = unquote(nested_entity_path)
quote do
current_opts = Process.get(:builder_opts, [])
current_opts = Process.get({:builder_opts, unquote(nested_entity_path)}, [])
Process.put(:builder_opts, Keyword.put(current_opts, unquote(key), unquote(value)))
Process.put(
{:builder_opts, unquote(nested_entity_path)},
Keyword.put(current_opts, unquote(key), unquote(value))
)
end
end
end

View file

@ -20,7 +20,7 @@ defmodule Ash.Dsl.Section do
For a full example, see `Ash.Dsl.Extension`.
"""
defstruct [:name, schema: [], describe: "", entities: [], sections: []]
defstruct [:name, imports: [], schema: [], describe: "", entities: [], sections: []]
@type t :: %__MODULE__{
name: atom,

View file

@ -40,7 +40,7 @@ defmodule Ash.Resource do
alias Ash.Dsl.Extension
:persistent_term.put({__MODULE__, :data_layer}, @data_layer)
:persistent_term.put({__MODULE__, :ash, :authorizers}, @authorizers)
:persistent_term.put({__MODULE__, :authorizers}, @authorizers)
Extension.set_state(false)
@ -51,7 +51,7 @@ defmodule Ash.Resource do
@doc false
def build_dsl do
:persistent_term.put({__MODULE__, :data_layer}, @data_layer)
:persistent_term.put({__MODULE__, :ash, :authorizers}, @authorizers)
:persistent_term.put({__MODULE__, :authorizers}, @authorizers)
Extension.set_state(true)
:ok

View file

@ -41,7 +41,7 @@ defmodule Ash.Resource.Relationships.ManyToMany do
type: :atom,
required: true,
doc:
"The field on the join table that should line up with `source_field` on this resource. Default: [resource_name]_id"
"The field on the join table that should line up with `source_field` on this resource."
],
destination_field_on_join_table: [
type: :atom,

View file

@ -13,7 +13,7 @@ defmodule Ash.Resource.Relationships.SharedOptions do
destination_field: [
type: :atom,
doc:
"The field on the related resource that should match the `source_field` on this resource. Default: [resource.name]_id"
"The field on the related resource that should match the `source_field` on this resource."
],
source_field: [
type: :atom,

View file

@ -233,9 +233,6 @@ defmodule Ash.Type do
end
defp ash_type_module?(module) do
:attributes
|> module.module_info()
|> Keyword.get(:behaviour, [])
|> Enum.any?(&(&1 == __MODULE__))
Ash.implements_behaviour?(module, __MODULE__)
end
end

View file

@ -0,0 +1,97 @@
defmodule Mix.Tasks.Ash.Formatter do
@moduledoc "Generates a .formatter.exs from a list of extensions, and writes it."
use Mix.Task
@formatter_exs_template """
# THIS FILE IS AUTOGENERATED USING `mix ash.formatter`
# DONT MODIFY IT BY HAND
locals_without_parens = __replace_me__
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
locals_without_parens: locals_without_parens,
export: [
locals_without_parens: locals_without_parens
]
]
"""
@shortdoc @moduledoc
def run(opts) do
{opts, []} = OptionParser.parse!(opts, strict: [check: :boolean, extensions: :string])
unless opts[:extensions] do
raise "Must supply a comma separated list of extensions to generate a .formatter.exs for"
end
locals_without_parens =
opts[:extensions]
|> String.split(",")
|> Enum.flat_map(fn extension ->
extension_mod = Module.concat([extension])
all_entity_builders(extension_mod.sections())
end)
|> Enum.uniq()
|> Enum.sort()
contents =
@formatter_exs_template
|> String.replace("__replace_me__", inspect(locals_without_parens))
|> Code.format_string!()
# |> IO.puts()
contents_with_newline = [contents, "\n"]
if opts[:check] do
if File.read!(".formatter.exs") != IO.iodata_to_binary(contents_with_newline) do
raise """
.formatter.exs is not up to date!
Run the following command and commit the result:
mix ash.formatter --extensions #{opts[:extensions]}
"""
else
IO.puts("The current .formatter.exs is correct")
end
else
File.write!(".formatter.exs", contents_with_newline)
end
end
defp all_entity_builders(sections) do
Enum.flat_map(sections, fn section ->
Enum.concat([
entity_option_builders(section),
section_option_builders(section),
entity_builders(section)
])
end)
end
defp entity_builders(section) do
Enum.flat_map(section.entities, fn entity ->
arg_count = Enum.count(entity.args)
[{entity.name, arg_count}, {entity.name, arg_count + 1}]
end) ++ all_entity_builders(section.sections())
end
defp entity_option_builders(section) do
Enum.flat_map(section.entities, fn entity ->
entity.schema
|> Keyword.drop(entity.args)
|> Enum.map(fn {key, _schema} ->
{key, 1}
end)
end)
end
defp section_option_builders(section) do
Enum.map(section.schema, fn {key, _} ->
{key, 1}
end)
end
end

View file

@ -18,6 +18,7 @@ defmodule Ash.MixProject do
elixirc_paths: elixirc_paths(Mix.env()),
package: package(),
deps: deps(),
dialyzer: [plt_add_apps: [:mix]],
test_coverage: [tool: ExCoveralls],
preferred_cli_env: [
coveralls: :test,
@ -97,7 +98,8 @@ defmodule Ash.MixProject do
defp aliases do
[
sobelow: "sobelow --skip",
credo: "credo --strict"
credo: "credo --strict",
"ash.formatter": "ash.formatter --extensions Ash.Dsl,Ash.Api.Dsl"
]
end
end