mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
improvement: Forbid reserved field names (#388)
Co-authored-by: Juha Lehtonen <juha.lehtonen@relexsolutions.com>
This commit is contained in:
parent
ace38cd31f
commit
fdebbeb242
5 changed files with 109 additions and 8 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
60
lib/ash/resource/transformers/no_reserved_field_names.ex
Normal file
60
lib/ash/resource/transformers/no_reserved_field_names.ex
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue