improvement: Forbid reserved field names (#388)

Co-authored-by: Juha Lehtonen <juha.lehtonen@relexsolutions.com>
This commit is contained in:
Juha 2022-10-03 23:19:16 +03:00 committed by GitHub
parent ace38cd31f
commit fdebbeb242
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 109 additions and 8 deletions

View file

@ -275,4 +275,16 @@ defmodule Ash.Resource do
end
end
end
@doc false
def reserved_names do
[
:__struct__,
:__meta__,
:__metadata__,
:__order__,
:calculations,
:aggregates
]
end
end

View file

@ -1210,6 +1210,7 @@ defmodule Ash.Resource.Dsl do
Ash.Resource.Transformers.DefaultAccept,
Ash.Resource.Transformers.SetTypes,
Ash.Resource.Transformers.RequireUniqueFieldNames,
Ash.Resource.Transformers.NoReservedFieldNames,
Ash.Resource.Transformers.ValidateRelationshipAttributes,
Ash.Resource.Transformers.ValidateEagerIdentities,
Ash.Resource.Transformers.ValidateAggregatesSupported,

View file

@ -14,7 +14,8 @@ defmodule Ash.Schema do
@primary_key false
embedded_schema do
for attribute <- Ash.Resource.Info.attributes(__MODULE__) do
for attribute <- Ash.Resource.Info.attributes(__MODULE__),
attribute.name not in Ash.Resource.reserved_names() do
read_after_writes? = attribute.generated? and is_nil(attribute.default)
constraint_opts =
@ -57,7 +58,8 @@ defmodule Ash.Schema do
Module.put_attribute(__MODULE__, :ash_struct_fields, field)
end
for relationship <- Ash.Resource.Info.relationships(__MODULE__) do
for relationship <- Ash.Resource.Info.relationships(__MODULE__),
relationship.name not in Ash.Resource.reserved_names() do
Module.put_attribute(
__MODULE__,
:ash_struct_fields,
@ -65,7 +67,8 @@ defmodule Ash.Schema do
)
end
for aggregate <- Ash.Resource.Info.aggregates(__MODULE__) do
for aggregate <- Ash.Resource.Info.aggregates(__MODULE__),
aggregate.name not in Ash.Resource.reserved_names() do
{:ok, type} = Aggregate.kind_to_type(aggregate.kind, :string)
field(aggregate.name, Ash.Type.ecto_type(type), virtual: true)
@ -77,7 +80,8 @@ defmodule Ash.Schema do
)
end
for calculation <- Ash.Resource.Info.calculations(__MODULE__) do
for calculation <- Ash.Resource.Info.calculations(__MODULE__),
calculation.name not in Ash.Resource.reserved_names() do
{mod, _} = calculation.calculation
field(calculation.name, Ash.Type.ecto_type(calculation.type), virtual: true)
@ -102,7 +106,8 @@ defmodule Ash.Schema do
@primary_key false
schema Ash.DataLayer.source(__MODULE__) do
for attribute <- Ash.Resource.Info.attributes(__MODULE__) do
for attribute <- Ash.Resource.Info.attributes(__MODULE__),
attribute.name not in Ash.Resource.reserved_names() do
read_after_writes? = attribute.generated? and is_nil(attribute.default)
constraint_opts =
@ -145,7 +150,8 @@ defmodule Ash.Schema do
Module.put_attribute(__MODULE__, :ash_struct_fields, field)
end
for relationship <- Ash.Resource.Info.relationships(__MODULE__) do
for relationship <- Ash.Resource.Info.relationships(__MODULE__),
relationship.name not in Ash.Resource.reserved_names() do
Module.put_attribute(
__MODULE__,
:ash_struct_fields,
@ -153,7 +159,8 @@ defmodule Ash.Schema do
)
end
for aggregate <- Ash.Resource.Info.aggregates(__MODULE__) do
for aggregate <- Ash.Resource.Info.aggregates(__MODULE__),
aggregate.name not in Ash.Resource.reserved_names() do
{:ok, type} = Aggregate.kind_to_type(aggregate.kind, :string)
field(aggregate.name, Ash.Type.ecto_type(type), virtual: true)
@ -165,7 +172,8 @@ defmodule Ash.Schema do
)
end
for calculation <- Ash.Resource.Info.calculations(__MODULE__) do
for calculation <- Ash.Resource.Info.calculations(__MODULE__),
calculation.name not in Ash.Resource.reserved_names() do
{mod, _} = calculation.calculation
field(calculation.name, Ash.Type.ecto_type(calculation.type), virtual: true)

View file

@ -0,0 +1,60 @@
defmodule Ash.Resource.Transformers.NoReservedFieldNames do
@moduledoc """
Confirms that a resource does not use reserved names for field names.
Reserved field names are: #{inspect(Ash.Resource.reserved_names())}.
"""
use Spark.Dsl.Transformer
alias Spark.Dsl.Transformer
alias Spark.Error.DslError
def transform(dsl_state) do
resource = Transformer.get_persisted(dsl_state, :module)
attributes =
dsl_state
|> Transformer.get_entities([:attributes])
relationships =
dsl_state
|> Transformer.get_entities([:relationships])
calculations =
dsl_state
|> Transformer.get_entities([:calculations])
aggregates =
dsl_state
|> Transformer.get_entities([:aggregates])
attributes
|> Enum.concat(relationships)
|> Enum.concat(calculations)
|> Enum.concat(aggregates)
|> Enum.each(fn %{name: name} = field ->
if name in Ash.Resource.reserved_names() do
path =
case field do
%Ash.Resource.Aggregate{kind: kind} -> [:aggregates, kind, name]
%Ash.Resource.Calculation{} -> [:calculations, name]
%Ash.Resource.Attribute{} -> [:attributes, name]
%{type: type} -> [:relationships, type, name]
end
raise DslError,
message: """
Field #{name} is using a reserved name.
Reserved field names are: #{inspect(Ash.Resource.reserved_names())}
""",
path: path,
module: resource
end
end)
{:ok, dsl_state}
end
def after_compile?, do: true
end

View file

@ -91,5 +91,25 @@ defmodule Ash.Test.Resource.AttributesTest do
end
)
end
test "raises if you pass a reserved name for `name`" do
for name <- Ash.Resource.reserved_names() do
assert_raise(
Spark.Error.DslError,
~r/Field #{name} is using a reserved name/,
fn ->
defmodule :"Elixir.Resource#{name}" do
@moduledoc false
use Ash.Resource
attributes do
uuid_primary_key :id
attribute name, :string
end
end
end
)
end
end
end
end