diff --git a/.formatter.exs b/.formatter.exs index b055c2cb..27f6ee82 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -41,6 +41,8 @@ locals_without_parens = [ destroy: 1, destroy: 2, dispatcher: 1, + entry: 1, + entry: 2, error_handler: 1, event: 1, expensive?: 1, @@ -91,6 +93,7 @@ locals_without_parens = [ read: 1, read: 2, read_action: 1, + registry: 1, reject: 1, relationship_context: 1, require_attributes: 1, diff --git a/documentation/introduction/getting_started.md b/documentation/introduction/getting_started.md index 42aad6d8..5a72f777 100644 --- a/documentation/introduction/getting_started.md +++ b/documentation/introduction/getting_started.md @@ -52,7 +52,7 @@ attribute(:id, :integer, allow_nil?: true) ## Create an Ash API -Create an API module. This will be your primary way to interact with your Ash resources. We recommend `lib/my_app/api.ex` for simple setups. For more information on organizing resources into contexts/domains, see the [Contexts and Domains](contexts_and_domains.html) guide. +Create an API module. This will be your primary way to interact with your Ash resources. We recommend `lib/my_app/api.ex` for simple setups. ```elixir # lib/my_app/api.ex @@ -64,6 +64,33 @@ defmodule MyApp.Api do end ``` +## Create a registry + +The registry is in charge of keeping track of the resources available to an api. + +```elixir +# lib/my_app/registry.ex +defmodule MyApp.Registry do + use Ash.Registry + + entries do + end +end +``` + +## Refer to that registry in your api + +```elixir +# lib/my_app/api.ex +defmodule MyApp.Api do + use Ash.Api + + resources do + registry MyApp.Registry + end +end +``` + ## Create a resource A resource is the primary entity in Ash. Your API module ties your resources together and gives them an interface, but the vast majority of your configuration will live in resources. @@ -123,12 +150,12 @@ For full details on defining a resource, see: `Ash.Resource.Dsl`. ## Add resources to your API -Alter your API (`lib/my_app/api.ex`) to add the resources we created on the previous step: +Alter your Registry (`lib/my_app/registry.ex`) to add the resources we created on the previous step: ```elixir -resources do - resource MyApp.User - resource MyApp.Tweet +entries do + entry MyApp.User + entry MyApp.Tweet end ``` @@ -192,12 +219,12 @@ iex(6)> changeset = Ash.Changeset.new(MyApp.User, %{email: "@eng.com"}) To be able to store and later on read your resources, a _data layer_ is required. For more information, see the documentation for the data layer you would like to use. The currently supported data layers are listed below: -| Storage | Datalayer | Storage Documentation | -| --- | ---| --- | -| postgres | [AshPostgres.DataLayer](https://hexdocs.pm/ash_postgres) | [Postgres Documentation](https://www.postgresql.org/docs/) | -| csv | [AshCsv.DataLayer](https://hexdocs.pm/ash_csv) | [CSV Information](https://en.wikipedia.org/wiki/Comma-separated_values) | -| ets | `Ash.DataLayer.Ets` | [Erlang Term Storage Documentation](https://erlang.org/doc/man/ets.html) | -| mnesia | `Ash.DataLayer.Mnesia` | [Mnesia Documentation](https://erlang.org/doc/man/mnesia.html) | +| Storage | Datalayer | Storage Documentation | +| -------- | -------------------------------------------------------- | ------------------------------------------------------------------------ | +| postgres | [AshPostgres.DataLayer](https://hexdocs.pm/ash_postgres) | [Postgres Documentation](https://www.postgresql.org/docs/) | +| csv | [AshCsv.DataLayer](https://hexdocs.pm/ash_csv) | [CSV Information](https://en.wikipedia.org/wiki/Comma-separated_values) | +| ets | `Ash.DataLayer.Ets` | [Erlang Term Storage Documentation](https://erlang.org/doc/man/ets.html) | +| mnesia | `Ash.DataLayer.Mnesia` | [Mnesia Documentation](https://erlang.org/doc/man/mnesia.html) | To add a data layer, we need to add it to the `use Ash.Resource` statement. In this case we are going to use ETS which is a in-memory data layer that is built diff --git a/documentation/topics/contexts_and_domains.md b/documentation/topics/contexts_and_domains.md deleted file mode 100644 index 502bdc81..00000000 --- a/documentation/topics/contexts_and_domains.md +++ /dev/null @@ -1,14 +0,0 @@ -# Contexts and Domains - -It is suggested that you read a bit on Domain Driven Design before proceeding. If you are using phoenix or are familiar with phoenix contexts, then this will make sense to you. - -In order to support domain driven design, Ash supports defining multiple APIs, each with their own set of resources. It is possible to share a resource between APIs, but this gets untenable very quickly because any resources related to the shared resource must _both_ appear in each API. - -An experimental "Delegation" data layer was added to allow you to use other resources in other APIs as the data layer for a resource, but it created a significant amount of complexity in determining data layer behavior. Instead, simply use the same data layer and configuration in both resources. - -Things missing to make this work well: - -- Define the ecto schema as a separate module (prerequisite for hidden attributes) -- "hidden" attributes - attributes that are defined on the schema but not the Ash struct -- ability to filter on hidden fields in certain places (haven't determined where this needs to happen) -- ability to add a "base_filter" that can leverage hidden attributes diff --git a/lib/ash/api/api.ex b/lib/ash/api/api.ex index 5cccd749..8f76f943 100644 --- a/lib/ash/api/api.ex +++ b/lib/ash/api/api.ex @@ -6,12 +6,20 @@ defmodule Ash.Api do for all resources in that Api. You include them in an Api like so: ```elixir + defmodule MyApp.Registry do + use Ash.Registry + + entries do + entry OneResource + entry SecondResource + end + end + defmodule MyApp.Api do use Ash.Api resources do - resource OneResource - resource SecondResource + registry MyApp.Registry end end ``` @@ -477,20 +485,40 @@ defmodule Ash.Api do end def resource(api, resource) do - api - |> resource_references() - |> Enum.find(&(&1.resource == resource || &1.as == resource)) - |> case do - nil -> {:error, NoSuchResource.exception(resource: resource)} - reference -> {:ok, reference.resource} + if Ash.Resource.Info.embedded?(resource) do + {:ok, resource} + else + api + |> resources() + |> Enum.find(&(&1 == resource)) + |> case do + nil -> {:error, NoSuchResource.exception(resource: resource)} + resource -> {:ok, resource} + end end end - @spec resources(Ash.Api.t()) :: [Ash.Resource.t()] + @spec resources(Ash.Api.t()) :: list(Ash.Resource.t()) def resources(api) do api |> Extension.get_entities([:resources]) |> Enum.map(& &1.resource) + |> case do + [] -> + if registry = registry(api) do + Ash.Registry.entries(registry) + else + [] + end + + other -> + other + end + end + + @spec registry(atom) :: atom | nil + def registry(api) do + Extension.get_opt(api, [:resources], :registry, nil) end @spec define_interfaces?(atom) :: boolean @@ -498,11 +526,6 @@ defmodule Ash.Api do Extension.get_opt(api, [:resources], :define_interfaces?, false) end - @spec resource_references(Ash.Api.t()) :: [Ash.Api.ResourceReference.t()] - def resource_references(api) do - Extension.get_entities(api, [:resources]) - end - @doc false @spec get!(Ash.Api.t(), Ash.Resource.t(), term(), Keyword.t()) :: Ash.Resource.record() | no_return diff --git a/lib/ash/api/dsl.ex b/lib/ash/api/dsl.ex index 50e05734..6acc0a6a 100644 --- a/lib/ash/api/dsl.ex +++ b/lib/ash/api/dsl.ex @@ -3,7 +3,6 @@ defmodule Ash.Api.Dsl do name: :resource, describe: "A reference to a resource", target: Ash.Api.ResourceReference, - modules: [:resource], args: [:resource], examples: [ "resource MyApp.User" @@ -38,14 +37,46 @@ defmodule Ash.Api.Dsl do Keep in mind that this can increase the compile times of your application. """ + ], + registry: [ + type: :atom, + # {:ash_behaviour, Ash.Registry}, + doc: """ + Allows declaring that only the modules in a certain registry should be allowed to work with this Api. + + This option is ignored if any explicit resources are included in the api, so everything is either in the registry + or in the api. See the docs on `Ash.Registry` for what the registry is used for. + """ ] ], + modules: [:registry], + deprecations: [ + resource: """ + Please define your resources in an `Ash.Registry`. For example: + + # my_app/my_api/registry.ex + defmodule MyApp.MyApi.Registry do + use Ash.Registry + + entries do + entry MyApp.Post + entry MyApp.Comment + end + end + + # In your api module + resources do + registry MyApp.MyApi.Registry + end + """ + ], entities: [ @resource ] } @transformers [ + Ash.Api.Transformers.EnsureResourcesCompiled, Ash.Api.Transformers.ValidateRelatedResourceInclusion, Ash.Api.Transformers.ValidateRelationshipAttributes, Ash.Api.Transformers.ValidateManyToManyJoinAttributes diff --git a/lib/ash/api/interface.ex b/lib/ash/api/interface.ex index 77173a3a..0243c097 100644 --- a/lib/ash/api/interface.ex +++ b/lib/ash/api/interface.ex @@ -146,83 +146,6 @@ defmodule Ash.Api.Interface do end end - @doc false - def set_tenant(query_or_changeset, opts) do - case Keyword.fetch(opts, :tenant) do - {:ok, tenant} -> - case query_or_changeset do - %Ash.Query{} = query -> - Ash.Query.set_tenant(query, tenant) - - %Ash.Changeset{} = changeset -> - Ash.Changeset.set_tenant(changeset, tenant) - - other -> - other - end - - :error -> - query_or_changeset - end - end - - @doc false - def action_interface(api) do - api - |> Ash.Api.resource_references() - |> Enum.flat_map(fn %{resource: resource} = reference -> - resource_name = name(reference) - - resource - |> Ash.Resource.Info.actions() - |> Enum.map(fn action -> - {resource, resource_name, action} - end) - end) - end - - @doc false - def getters(api) do - api - |> Ash.Api.resource_references() - |> Enum.flat_map(fn %{resource: resource} = reference -> - if Ash.Resource.Info.primary_action(resource, :read) do - resource_name = name(reference) - - resource - |> Ash.Resource.Info.identities() - |> Enum.map(fn identity -> - {resource, resource_name, identity} - end) - else - [] - end - end) - end - - @doc false - def resources_with_names(api) do - api - |> Ash.Api.resource_references() - |> Enum.map(fn ref -> - {ref.resource, name(ref)} - end) - end - - @doc false - def name(reference) do - reference - |> Map.get(:as) - |> Kernel.||( - reference.resource - |> Module.split() - |> List.last() - |> to_string() - |> Macro.underscore() - ) - |> to_string() - end - defmacro enforce_query_or_resource!(query_or_resource) do quote generated: true do case Ash.Api.Interface.do_enforce_query_or_resource!(unquote(query_or_resource)) do diff --git a/lib/ash/api/resource_reference.ex b/lib/ash/api/resource_reference.ex index 4ceabbf4..fb7bb8d0 100644 --- a/lib/ash/api/resource_reference.ex +++ b/lib/ash/api/resource_reference.ex @@ -1,7 +1,7 @@ defmodule Ash.Api.ResourceReference do @moduledoc "Represents a resource in an API" - defstruct [:resource, :as] + defstruct [:resource] @type t :: %__MODULE__{} end diff --git a/lib/ash/api/transformers/ensure_resources_compiled.ex b/lib/ash/api/transformers/ensure_resources_compiled.ex index b520c7be..dd576ed3 100644 --- a/lib/ash/api/transformers/ensure_resources_compiled.ex +++ b/lib/ash/api/transformers/ensure_resources_compiled.ex @@ -6,19 +6,47 @@ defmodule Ash.Api.Transformers.EnsureResourcesCompiled do """ use Ash.Dsl.Transformer - alias Ash.Dsl.Transformer require Logger @impl true def after_compile?, do: true @impl true - def transform(_api, dsl) do - dsl - |> Transformer.get_entities([:resources]) - |> Enum.map(& &1.resource) - |> Enum.each(fn resource -> - resource.ash_dsl_config() + def transform(api, dsl) do + api + |> Ash.Api.resources() + |> Enum.map(fn resource -> + try do + # This is to get the compiler to ensure that the resource is compiled + # For some very strange reason, `Code.ensure_compiled/1` isn't enough + resource.ash_dsl_config() + rescue + _ -> + :ok + end + + case Code.ensure_compiled(resource) do + {:module, _module} -> + false + + {:error, error} -> + # The module is being compiled but is in a deadlock that may or may not be resolved + {resource, error} + end end) + |> Enum.filter(& &1) + |> case do + [] -> + {:ok, dsl} + + rejected -> + for {resource, error} <- rejected do + Logger.error( + "Could not ensure that #{inspect(resource)} was compiled: #{inspect(error)}" + ) + end + + :halt + end end end diff --git a/lib/ash/api/transformers/unique_function_names.ex b/lib/ash/api/transformers/unique_function_names.ex deleted file mode 100644 index d095ea15..00000000 --- a/lib/ash/api/transformers/unique_function_names.ex +++ /dev/null @@ -1,40 +0,0 @@ -defmodule Ash.Api.Transformers.UniqueFunctionNames do - @moduledoc """ - Ensures that all function names added to the API will be unique. - """ - use Ash.Dsl.Transformer - - alias Ash.Dsl.Transformer - require Logger - - # sobelow_skip ["DOS.BinToAtom"] - def transform(_module, dsl) do - dsl - |> Transformer.get_entities([:resources]) - |> Enum.flat_map(fn %{resource: resource} = reference -> - resource - |> Ash.Resource.Info.actions() - |> Enum.map(fn action -> - action.as || :"#{Ash.Api.Interface.name(reference)}_#{action.name}" - end) - end) - |> Enum.reduce(%{}, fn name, acc -> - Map.update(acc, name, 0, &(&1 + 1)) - end) - |> Enum.each(fn {name, count} -> - if count > 1 do - raise Ash.Error.Dsl.DslError.exception( - module: __MODULE__, - message: """ - Multiple actions would share the Api helper name #{name}. - Specify the `as` option to update the helper name for those actions so they do not match. - Alternatively, specify the `as` option for those resources so they do not have the same short name. - """, - path: [:actions] - ) - end - end) - - {:ok, dsl} - end -end diff --git a/lib/ash/api/transformers/validate_many_to_many_join_attributes.ex b/lib/ash/api/transformers/validate_many_to_many_join_attributes.ex index 7dfb2290..99ffdf5e 100644 --- a/lib/ash/api/transformers/validate_many_to_many_join_attributes.ex +++ b/lib/ash/api/transformers/validate_many_to_many_join_attributes.ex @@ -4,8 +4,6 @@ defmodule Ash.Api.Transformers.ValidateManyToManyJoinAttributes do """ use Ash.Dsl.Transformer - alias Ash.Dsl.Transformer - @impl true def after_compile?, do: true @@ -14,10 +12,9 @@ defmodule Ash.Api.Transformers.ValidateManyToManyJoinAttributes do def after?(_), do: false @impl true - def transform(_api, dsl) do - dsl - |> Transformer.get_entities([:resources]) - |> Enum.map(& &1.resource) + def transform(api, dsl) do + api + |> Ash.Api.resources() |> Enum.each(fn resource -> resource |> Ash.Resource.Info.relationships() diff --git a/lib/ash/api/transformers/validate_relationship_attributes.ex b/lib/ash/api/transformers/validate_relationship_attributes.ex index a4d4fd6a..e6e1f0c2 100644 --- a/lib/ash/api/transformers/validate_relationship_attributes.ex +++ b/lib/ash/api/transformers/validate_relationship_attributes.ex @@ -4,8 +4,6 @@ defmodule Ash.Api.Transformers.ValidateRelationshipAttributes do """ use Ash.Dsl.Transformer - alias Ash.Dsl.Transformer - @impl true def after_compile?, do: true @@ -14,10 +12,9 @@ defmodule Ash.Api.Transformers.ValidateRelationshipAttributes do def after?(_), do: false @impl true - def transform(_api, dsl) do - dsl - |> Transformer.get_entities([:resources]) - |> Enum.map(& &1.resource) + def transform(api, dsl) do + api + |> Ash.Api.resources() |> Enum.each(fn resource -> attribute_names = resource diff --git a/lib/ash/dsl/entity.ex b/lib/ash/dsl/entity.ex index 40b3fc4a..a11a6745 100644 --- a/lib/ash/dsl/entity.ex +++ b/lib/ash/dsl/entity.ex @@ -38,6 +38,7 @@ defmodule Ash.Dsl.Entity do :transform, examples: [], entities: [], + deprecations: [], describe: "", snippet: "", args: [], diff --git a/lib/ash/dsl/extension.ex b/lib/ash/dsl/extension.ex index 68137e5c..ebda08dc 100644 --- a/lib/ash/dsl/extension.ex +++ b/lib/ash/dsl/extension.ex @@ -106,9 +106,10 @@ defmodule Ash.Dsl.Extension do @callback transformers() :: [module] defp dsl!(resource) do + Code.ensure_compiled!(resource) resource.ash_dsl_config() rescue - UndefinedFunctionError -> + _ in [UndefinedFunctionError, ArgumentError] -> try do Module.get_attribute(resource, :ash_dsl_config) || %{} rescue @@ -711,7 +712,7 @@ defmodule Ash.Dsl.Extension do def do_build_section(mod, extension, section, path) do entity_modules = Enum.map(section.entities, fn entity -> - build_entity(mod, extension, path ++ [section.name], entity) + build_entity(mod, extension, path ++ [section.name], entity, section.deprecations) end) section_modules = @@ -768,6 +769,13 @@ defmodule Ash.Dsl.Extension do extension = unquote(extension) section = unquote(Macro.escape(section)) + Ash.Dsl.Extension.maybe_deprecated( + field, + section.deprecations, + section_path, + __CALLER__ + ) + value = if field in section.modules do Ash.Dsl.Extension.expand_alias(value, __CALLER__) @@ -811,7 +819,7 @@ defmodule Ash.Dsl.Extension do end @doc false - def build_entity(mod, extension, section_path, entity, nested_entity_path \\ []) do + def build_entity(mod, extension, section_path, entity, deprecations, nested_entity_path \\ []) do nested_entity_parts = Enum.map(nested_entity_path, &Macro.camelize(to_string(&1))) mod_parts = @@ -831,6 +839,7 @@ defmodule Ash.Dsl.Extension do extension, section_path, entity, + entity.deprecations, nested_entity_path ++ [key] ) end) @@ -840,6 +849,7 @@ defmodule Ash.Dsl.Extension do options_mod_name, entity.schema, entity.modules, + entity.deprecations, nested_entity_path ) @@ -854,7 +864,8 @@ defmodule Ash.Dsl.Extension do section_path: Macro.escape(section_path), options_mod_name: Macro.escape(options_mod_name), nested_entity_mods: Macro.escape(nested_entity_mods), - nested_entity_path: Macro.escape(nested_entity_path) + nested_entity_path: Macro.escape(nested_entity_path), + deprecations: deprecations ] do @moduledoc false defmacro unquote(entity.name)(unquote_splicing(args), opts \\ []) do @@ -863,16 +874,32 @@ defmodule Ash.Dsl.Extension do entity = unquote(Macro.escape(entity)) entity_name = unquote(Macro.escape(entity.name)) entity_args = unquote(Macro.escape(entity.args)) + entity_deprecations = unquote(entity.deprecations) options_mod_name = unquote(Macro.escape(options_mod_name)) source = unquote(__MODULE__) extension = unquote(Macro.escape(extension)) nested_entity_mods = unquote(Macro.escape(nested_entity_mods)) nested_entity_path = unquote(Macro.escape(nested_entity_path)) + deprecations = unquote(deprecations) + + Ash.Dsl.Extension.maybe_deprecated( + entity.name, + deprecations, + section_path ++ nested_entity_path, + __CALLER__ + ) arg_values = entity_args |> Enum.zip(unquote(args)) |> Enum.map(fn {key, value} -> + Ash.Dsl.Extension.maybe_deprecated( + key, + entity_deprecations, + nested_entity_path, + __CALLER__ + ) + if key in entity.modules do Ash.Dsl.Extension.expand_alias(value, __CALLER__) else @@ -882,6 +909,13 @@ defmodule Ash.Dsl.Extension do opts = Enum.map(opts, fn {key, value} -> + Ash.Dsl.Extension.maybe_deprecated( + key, + entity_deprecations, + nested_entity_path, + __CALLER__ + ) + if key in entity.modules do {key, Ash.Dsl.Extension.expand_alias(value, __CALLER__)} else @@ -1019,13 +1053,14 @@ defmodule Ash.Dsl.Extension do end @doc false - def build_entity_options(module_name, schema, modules, nested_entity_path) do + def build_entity_options(module_name, schema, modules, deprecations, nested_entity_path) do Module.create( module_name, quote bind_quoted: [ schema: Macro.escape(schema), nested_entity_path: nested_entity_path, - modules: modules + modules: modules, + deprecations: deprecations ] do @moduledoc false @@ -1034,6 +1069,9 @@ defmodule Ash.Dsl.Extension do key = unquote(key) nested_entity_path = unquote(nested_entity_path) modules = unquote(modules) + deprecations = unquote(deprecations) + + Ash.Dsl.Extension.maybe_deprecated(key, deprecations, nested_entity_path, __CALLER__) value = if key in modules do @@ -1059,6 +1097,22 @@ defmodule Ash.Dsl.Extension do module_name end + @doc false + def maybe_deprecated(field, deprecations, path, env) do + if Keyword.has_key?(deprecations, field) do + prefix = + case Enum.join(path) do + "" -> "" + path -> "#{path}." + end + + IO.warn( + "The #{prefix}#{field} key will be deprecated in an upcoming release!\n\n#{deprecations[field]}", + Macro.Env.stacktrace(env) + ) + end + end + def expand_alias(ast, env) do Macro.postwalk(ast, fn {first, {:__aliases__, _, _} = node} -> diff --git a/lib/ash/dsl/section.ex b/lib/ash/dsl/section.ex index 8ffb141a..e37dbaf5 100644 --- a/lib/ash/dsl/section.ex +++ b/lib/ash/dsl/section.ex @@ -28,6 +28,7 @@ defmodule Ash.Dsl.Section do snippet: "", examples: [], modules: [], + deprecations: [], entities: [], sections: [], docs: "" diff --git a/lib/ash/embeddable_type.ex b/lib/ash/embeddable_type.ex index 25ab2d6e..c0f52a1c 100644 --- a/lib/ash/embeddable_type.ex +++ b/lib/ash/embeddable_type.ex @@ -47,6 +47,11 @@ defmodule Ash.EmbeddableType do ] ] + defmodule ShadowApi do + @moduledoc false + use Ash.Api + end + @doc false def embedded_resource_array_constraints, do: @embedded_resource_array_constraints @@ -135,7 +140,7 @@ defmodule Ash.EmbeddableType do defmacro single_embed_implementation do # credo:disable-for-next-line Credo.Check.Refactor.LongQuoteBlocks quote location: :keep do - alias __MODULE__.ShadowApi + alias Ash.EmbeddableType.ShadowApi def storage_type, do: :map def cast_input(%{__struct__: __MODULE__} = input, _constraints), do: {:ok, input} @@ -375,7 +380,7 @@ defmodule Ash.EmbeddableType do defmacro array_embed_implementation do # credo:disable-for-next-line Credo.Check.Refactor.LongQuoteBlocks quote location: :keep do - alias __MODULE__.ShadowApi + alias Ash.EmbeddableType.ShadowApi def array_constraints, do: Ash.EmbeddableType.embedded_resource_array_constraints() def apply_constraints_array([], _constraints), do: {:ok, []} @@ -555,19 +560,6 @@ defmodule Ash.EmbeddableType do quote location: :keep do use Ash.Type - parent = __MODULE__ - - defmodule ShadowApi do - @moduledoc false - use Ash.Api - - @parent parent - - resources do - resource @parent, [] - end - end - Ash.EmbeddableType.single_embed_implementation() Ash.EmbeddableType.array_embed_implementation() end diff --git a/lib/ash/registry/dsl.ex b/lib/ash/registry/dsl.ex index bb998306..7382efaa 100644 --- a/lib/ash/registry/dsl.ex +++ b/lib/ash/registry/dsl.ex @@ -1,56 +1,48 @@ -# defmodule Ash.Registry.Dsl do -# @entry %Ash.Dsl.Entity{ -# name: :entry, -# describe: "A reference to a a module", -# target: Ash.Registry.EntryReference, -# args: [:entry], -# examples: [ -# "entry MyApp.Post" -# ], -# schema: [ -# entry: [ -# type: :atom, -# required: true, -# doc: "The module of the entry" -# ] -# ] -# } +defmodule Ash.Registry.Dsl do + @entry %Ash.Dsl.Entity{ + name: :entry, + describe: "A reference to an ash module (typically a resource)", + target: Ash.Registry.Entry, + args: [:entry], + examples: [ + "entry MyApp.User" + ], + schema: [ + entry: [ + type: :atom, + required: true, + doc: "The referenced module" + ] + ] + } -# @entries %Ash.Dsl.Section{ -# name: :entries, -# describe: "List the entries present in this registry", -# examples: [ -# """ -# entries do -# entry MyApp.User -# entry MyApp.Post -# entry MyApp.Comment -# end -# """ -# ], -# entities: [ -# @entry -# ] -# } + @entries %Ash.Dsl.Section{ + name: :entries, + describe: "List the entries present in this registry", + examples: [ + """ + entries do + entry MyApp.User + entry MyApp.Post + entry MyApp.Comment + end + """ + ], + entities: [ + @entry + ] + } -# @sections [@resources] + @sections [@entries] -# @moduledoc """ -# A small DSL for declaring APIs + @moduledoc """ + A small DSL for declaring an `Ash.Registry`. -# Apis are the entrypoints for working with your resources. + # Table of Contents + #{Ash.Dsl.Extension.doc_index(@sections)} -# Apis may optionally include a list of resources, in which case they can be -# used as an `Ash.Registry` in various places. This is for backwards compatibility, -# but if at all possible you should define an `Ash.Registry` if you are using an extension -# that requires a list of resources. For example, most extensions look for two application -# environment variables called `:ash_apis` and `:ash_registries` to find any potential registries + #{Ash.Dsl.Extension.doc(@sections)} + """ -# # Table of Contents -# #{Ash.Dsl.Extension.doc_index(@sections)} - -# #{Ash.Dsl.Extension.doc(@sections)} -# """ - -# use Ash.Dsl.Extension, sections: @sections, transformers: @transformers -# end + use Ash.Dsl.Extension, sections: @sections +end diff --git a/lib/ash/registry/entry.ex b/lib/ash/registry/entry.ex new file mode 100644 index 00000000..659849e6 --- /dev/null +++ b/lib/ash/registry/entry.ex @@ -0,0 +1,7 @@ +defmodule Ash.Registry.Entry do + @moduledoc "Represents an entry in a registry" + + defstruct [:entry] + + @type t :: %__MODULE__{} +end diff --git a/lib/ash/registry/registry.ex b/lib/ash/registry/registry.ex new file mode 100644 index 00000000..24cc9316 --- /dev/null +++ b/lib/ash/registry/registry.ex @@ -0,0 +1,39 @@ +defmodule Ash.Registry do + @moduledoc """ + A registry allows you to separate your resources from your `api` module, to reduce improve compile times and reduce compile time dependencies. + + For example: + + ```elixir + defmodule MyApp.MyRegistry do + use Ash.Registry + + entries do + entry MyApp.Resource + entry MyApp.OtherResource + end + end + ``` + """ + + @type t :: module + + use Ash.Dsl, default_extensions: [extensions: [Ash.Registry.Dsl]] + + alias Ash.Dsl.Extension + + @spec entries(t()) :: list(module) + def entries(registry) do + case registry |> Extension.get_entities([:entries]) |> Enum.map(& &1.entry) do + [] -> + registry |> Extension.get_entities([:resources]) |> Enum.map(& &1.resource) + + other -> + other + end + end + + @spec api_or_api_and_registry(Ash.Api.t() | {Ash.Api.t(), t()}) :: {t(), t()} + def api_or_api_and_registry({api, registry}), do: {api, registry} + def api_or_api_and_registry(api), do: {api, api} +end diff --git a/mix.exs b/mix.exs index c393bc1f..b7825586 100644 --- a/mix.exs +++ b/mix.exs @@ -83,9 +83,6 @@ defmodule Ash.MixProject do "documentation/topics/embedded_resources.md": [ title: "Embedded Resources" ], - "documentation/topics/contexts_and_domains.md": [ - title: "Context And Domains" - ], "documentation/topics/multitenancy.md": [ title: "Multitenancy" ] @@ -211,7 +208,7 @@ defmodule Ash.MixProject do sobelow: "sobelow --skip", credo: "credo --strict", "ash.formatter": - "ash.formatter --extensions Ash.Resource.Dsl,Ash.Api.Dsl,Ash.DataLayer.Ets,Ash.DataLayer.Mnesia,Ash.Notifier.PubSub" + "ash.formatter --extensions Ash.Resource.Dsl,Ash.Api.Dsl,Ash.Registry.Dsl,Ash.DataLayer.Ets,Ash.DataLayer.Mnesia,Ash.Notifier.PubSub" ] end end diff --git a/test/actions/belongs_to_test.exs b/test/actions/belongs_to_test.exs index bd4a9e70..29824a00 100644 --- a/test/actions/belongs_to_test.exs +++ b/test/actions/belongs_to_test.exs @@ -75,13 +75,22 @@ defmodule Ash.Test.Actions.BelongsToTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(Post) + entry(Reviewer) + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource(Post) - resource(Reviewer) + registry Registry end end diff --git a/test/actions/create_test.exs b/test/actions/create_test.exs index 166fad7d..7b5cf70c 100644 --- a/test/actions/create_test.exs +++ b/test/actions/create_test.exs @@ -258,18 +258,27 @@ defmodule Ash.Test.Actions.CreateTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(Author) + entry(Post) + entry(Profile) + entry(ProfileWithBelongsTo) + entry(PostLink) + entry(Authorized) + entry(GeneratedPkey) + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource(Author) - resource(Post) - resource(Profile) - resource(ProfileWithBelongsTo) - resource(PostLink) - resource(Authorized) - resource(GeneratedPkey) + registry Registry end end diff --git a/test/actions/destroy_test.exs b/test/actions/destroy_test.exs index 5c83c2af..d395a869 100644 --- a/test/actions/destroy_test.exs +++ b/test/actions/destroy_test.exs @@ -114,14 +114,23 @@ defmodule Ash.Test.Actions.DestroyTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(Author) + entry(Post) + entry(Profile) + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource(Author) - resource(Post) - resource(Profile) + registry Registry end end diff --git a/test/actions/load_test.exs b/test/actions/load_test.exs index 1c3e12a4..fe13f676 100644 --- a/test/actions/load_test.exs +++ b/test/actions/load_test.exs @@ -114,15 +114,24 @@ defmodule Ash.Test.Actions.LoadTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(Author) + entry(Post) + entry(Category) + entry(PostCategory) + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource(Author) - resource(Post) - resource(Category) - resource(PostCategory) + registry Registry end end diff --git a/test/actions/multitenancy_test.exs b/test/actions/multitenancy_test.exs index ba329960..74d35dd1 100644 --- a/test/actions/multitenancy_test.exs +++ b/test/actions/multitenancy_test.exs @@ -95,13 +95,23 @@ defmodule Ash.Actions.MultitenancyTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(Comment) + entry(Post) + entry(User) + end + end + defmodule Api do + @moduledoc false use Ash.Api resources do - resource Comment - resource Post - resource User + registry Registry end end diff --git a/test/actions/pagination_test.exs b/test/actions/pagination_test.exs index 068de1dc..40f0184a 100644 --- a/test/actions/pagination_test.exs +++ b/test/actions/pagination_test.exs @@ -63,11 +63,20 @@ defmodule Ash.Actions.PaginationTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(User) + end + end + defmodule Api do use Ash.Api resources do - resource User + registry Registry end end diff --git a/test/actions/read_test.exs b/test/actions/read_test.exs index 0c131d63..cf0d4c2e 100644 --- a/test/actions/read_test.exs +++ b/test/actions/read_test.exs @@ -69,13 +69,22 @@ defmodule Ash.Test.Actions.ReadTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(Post) + entry(Author) + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource Post - resource Author + registry Registry end end diff --git a/test/actions/update_test.exs b/test/actions/update_test.exs index 07f4679f..0c148114 100644 --- a/test/actions/update_test.exs +++ b/test/actions/update_test.exs @@ -192,16 +192,25 @@ defmodule Ash.Test.Actions.UpdateTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(Author) + entry(Post) + entry(Profile) + entry(PostLink) + entry(Authorized) + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource(Author) - resource(Post) - resource(Profile) - resource(PostLink) - resource(Authorized) + registry Registry end end diff --git a/test/actions/validation_test.exs b/test/actions/validation_test.exs index 010dc79a..4ef2c131 100644 --- a/test/actions/validation_test.exs +++ b/test/actions/validation_test.exs @@ -39,11 +39,20 @@ defmodule Ash.Test.Actions.ValidationTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry Profile + end + end + defmodule Api do use Ash.Api resources do - resource Profile + registry Registry end end diff --git a/test/api/api_test.exs b/test/api/api_test.exs index c1ead4cb..6aec2505 100644 --- a/test/api/api_test.exs +++ b/test/api/api_test.exs @@ -12,15 +12,4 @@ defmodule Ash.Test.Resource.ApiTest do end end end - - defmacrop defapi(opts \\ [], do: body) do - quote do - defmodule Api do - @moduledoc false - use Ash.Api, unquote(opts) - - unquote(body) - end - end - end end diff --git a/test/ash/data_layer/ets_test.exs b/test/ash/data_layer/ets_test.exs index f4e47d85..c0298500 100644 --- a/test/ash/data_layer/ets_test.exs +++ b/test/ash/data_layer/ets_test.exs @@ -39,11 +39,20 @@ defmodule Ash.DataLayer.EtsTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry EtsTestUser + end + end + defmodule EtsApiTest do use Ash.Api resources do - resource EtsTestUser + registry Registry end end diff --git a/test/authorizer/authorizer_test.exs b/test/authorizer/authorizer_test.exs index d297e778..45d6e137 100644 --- a/test/authorizer/authorizer_test.exs +++ b/test/authorizer/authorizer_test.exs @@ -22,11 +22,20 @@ defmodule Ash.Test.Changeset.AuthorizerTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry Post + end + end + defmodule Api do use Ash.Api resources do - resource Post + registry Registry end end diff --git a/test/calculation_test.exs b/test/calculation_test.exs index ef22e47d..3cbc8165 100644 --- a/test/calculation_test.exs +++ b/test/calculation_test.exs @@ -61,12 +61,21 @@ defmodule Ash.Test.CalculationTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(User) + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource(User) + registry Registry end end diff --git a/test/changeset/changeset_test.exs b/test/changeset/changeset_test.exs index bc2ad970..24063085 100644 --- a/test/changeset/changeset_test.exs +++ b/test/changeset/changeset_test.exs @@ -184,17 +184,26 @@ defmodule Ash.Test.Changeset.ChangesetTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry Category + entry Author + entry PostCategory + entry Post + entry CompositeKeyPost + entry UniqueNamePerAuthor + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource Category - resource Author - resource PostCategory - resource Post - resource CompositeKeyPost - resource UniqueNamePerAuthor + registry Registry end end diff --git a/test/code_interface_test.exs b/test/code_interface_test.exs index 555725c2..13b23f8c 100644 --- a/test/code_interface_test.exs +++ b/test/code_interface_test.exs @@ -34,6 +34,15 @@ defmodule Ash.Test.CodeInterfaceTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(User) + end + end + defmodule Api do @moduledoc false use Ash.Api @@ -41,7 +50,7 @@ defmodule Ash.Test.CodeInterfaceTest do resources do define_interfaces?(true) - resource(User) + registry Registry end end diff --git a/test/embedded_resource_test.exs b/test/embedded_resource_test.exs index cdc32485..86ad86c0 100644 --- a/test/embedded_resource_test.exs +++ b/test/embedded_resource_test.exs @@ -137,12 +137,21 @@ defmodule Ash.Test.Changeset.EmbeddedResourceTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(Author) + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource Author + registry Registry end end diff --git a/test/filter/filter_interaction_test.exs b/test/filter/filter_interaction_test.exs index 9ed75635..06197c3d 100644 --- a/test/filter/filter_interaction_test.exs +++ b/test/filter/filter_interaction_test.exs @@ -122,15 +122,24 @@ defmodule Ash.Test.Filter.FilterInteractionTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(Post) + entry(User) + entry(Profile) + entry(PostLink) + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource(Post) - resource(User) - resource(Profile) - resource(PostLink) + registry Registry end end diff --git a/test/filter/filter_test.exs b/test/filter/filter_test.exs index 8c7f6778..16c6e5c6 100644 --- a/test/filter/filter_test.exs +++ b/test/filter/filter_test.exs @@ -166,16 +166,25 @@ defmodule Ash.Test.Filter.FilterTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(Post) + entry(SoftDeletePost) + entry(User) + entry(Profile) + entry(PostLink) + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource(Post) - resource(SoftDeletePost) - resource(User) - resource(Profile) - resource(PostLink) + registry Registry end end diff --git a/test/notifier/notifier_test.exs b/test/notifier/notifier_test.exs index 16577ba4..1862af32 100644 --- a/test/notifier/notifier_test.exs +++ b/test/notifier/notifier_test.exs @@ -103,13 +103,22 @@ defmodule Ash.Test.NotifierTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry Post + entry PostLink + entry Comment + end + end + defmodule Api do use Ash.Api resources do - resource Post - resource PostLink - resource Comment + registry Registry end end diff --git a/test/notifier/pubsub_test.exs b/test/notifier/pubsub_test.exs index 8a6a8925..612f6d84 100644 --- a/test/notifier/pubsub_test.exs +++ b/test/notifier/pubsub_test.exs @@ -46,11 +46,20 @@ defmodule Ash.Test.Notifier.PubSubTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry Post + end + end + defmodule Api do use Ash.Api resources do - resource Post + registry Registry end end diff --git a/test/query_test.exs b/test/query_test.exs index e66b117b..b4a0d527 100644 --- a/test/query_test.exs +++ b/test/query_test.exs @@ -31,11 +31,20 @@ defmodule Ash.Test.QueryTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry(User) + end + end + defmodule Api do use Ash.Api resources do - resource User + registry Registry end end diff --git a/test/resource/changes/load_test.exs b/test/resource/changes/load_test.exs index 362a33c0..f4ba9928 100644 --- a/test/resource/changes/load_test.exs +++ b/test/resource/changes/load_test.exs @@ -23,11 +23,20 @@ defmodule Ash.Test.Resource.Changes.LoadTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry Post + end + end + defmodule Api do use Ash.Api resources do - resource Post + registry Registry end end diff --git a/test/resource/relationships/belongs_to_test.exs b/test/resource/relationships/belongs_to_test.exs index c0ebe05c..ba697fce 100644 --- a/test/resource/relationships/belongs_to_test.exs +++ b/test/resource/relationships/belongs_to_test.exs @@ -212,11 +212,20 @@ defmodule Ash.Test.Resource.Relationships.BelongsToTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry Post + end + end + defmodule Api do use Ash.Api resources do - resource(Post) + registry Registry end end end diff --git a/test/sort/sort_test.exs b/test/sort/sort_test.exs index 31812529..a800df27 100644 --- a/test/sort/sort_test.exs +++ b/test/sort/sort_test.exs @@ -28,12 +28,21 @@ defmodule Ash.Test.Sort.SortTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry Post + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource(Post) + registry Registry end end diff --git a/test/type/ci_string_test.exs b/test/type/ci_string_test.exs index 03660444..7bf38c86 100644 --- a/test/type/ci_string_test.exs +++ b/test/type/ci_string_test.exs @@ -33,12 +33,21 @@ defmodule Ash.Test.Type.CiString do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry Post + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource(Post) + registry Registry end end diff --git a/test/type/enum_test.exs b/test/type/enum_test.exs index f0c88d88..eeebe1b0 100644 --- a/test/type/enum_test.exs +++ b/test/type/enum_test.exs @@ -24,12 +24,21 @@ defmodule Ash.Test.Type.EnumTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry Post + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource(Post) + registry Registry end end diff --git a/test/type/string_test.exs b/test/type/string_test.exs index dcb47369..57d9e78e 100644 --- a/test/type/string_test.exs +++ b/test/type/string_test.exs @@ -32,12 +32,21 @@ defmodule Ash.Test.Type.StringTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry Post + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource(Post) + registry Registry end end diff --git a/test/type/type_test.exs b/test/type/type_test.exs index 90d435ae..291f8ecc 100644 --- a/test/type/type_test.exs +++ b/test/type/type_test.exs @@ -65,12 +65,21 @@ defmodule Ash.Test.Type.TypeTest do end end + defmodule Registry do + @moduledoc false + use Ash.Registry + + entries do + entry Post + end + end + defmodule Api do @moduledoc false use Ash.Api resources do - resource(Post) + registry Registry end end