Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/ash/actions/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1018,7 +1018,8 @@ defmodule Ash.Actions.Helpers do
end
end)
|> Map.new(fn attribute ->
{attribute.name, %Ash.NotLoaded{field: attribute.name, type: :attribute}}
{attribute.name,
%Ash.NotLoaded{field: attribute.name, type: :attribute, resource: resource}}
end)
end

Expand Down
28 changes: 20 additions & 8 deletions lib/ash/not_loaded.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,36 @@

defmodule Ash.NotLoaded do
@moduledoc "Used when a field hasn't been loaded or selected."
defstruct [:field, :type]
defstruct [:field, :type, :resource]

@type t :: %__MODULE__{
field: atom,
type: :relationship | :calculation | :aggregate | :attribute
type: :relationship | :calculation | :aggregate | :attribute,
resource: module | nil
}

defimpl Inspect do
import Inspect.Algebra

def inspect(not_loaded, opts) do
concat([
custom_options = Map.get(opts, :custom_options, [])
in_resource? = Keyword.get(custom_options, :in_resource?, false)
show_resource? = not is_nil(not_loaded.resource) and !in_resource?

container_doc(
"#Ash.NotLoaded<",
to_doc(not_loaded.type, opts),
", field: ",
to_doc(not_loaded.field, opts),
">"
])
[
to_doc(not_loaded.type, opts),
concat("field: ", to_doc(not_loaded.field, opts)),
or_empty(concat("resource: ", to_doc(not_loaded.resource, opts)), show_resource?)
],
">",
opts,
fn doc, _opts -> doc end
)
end

defp or_empty(value, true), do: value
defp or_empty(_, false), do: empty()
end
end
20 changes: 18 additions & 2 deletions lib/ash/resource.ex
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,15 @@ defmodule Ash.Resource do
fields = @show_fields -- subtract_fields

fun = fn field, opts ->
custom_options = Map.get(opts, :custom_options, [])

opts =
Map.put(
opts,
:custom_options,
Keyword.put(custom_options, :in_resource?, true)
)

Inspect.List.keyword({field, Map.get(record, field)}, opts)
end

Expand Down Expand Up @@ -618,8 +627,15 @@ defmodule Ash.Resource do
Map.get(record, resource_calculation.name)
else
case Map.fetch(record.calculations, resource_calculation.name) do
{:ok, value} -> value
:error -> %Ash.NotLoaded{type: :calculation, field: resource_calculation.name}
{:ok, value} ->
value

:error ->
%Ash.NotLoaded{
type: :calculation,
field: resource_calculation.name,
resource: record.__struct__
}
end
end

Expand Down
26 changes: 20 additions & 6 deletions lib/ash/resource/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ defmodule Ash.Schema do
Module.put_attribute(
__MODULE__,
:ash_struct_fields,
{relationship.name, %Ash.NotLoaded{type: :relationship, field: relationship.name}}
{relationship.name,
%Ash.NotLoaded{
type: :relationship,
field: relationship.name,
resource: __MODULE__
}}
)
end

Expand All @@ -110,7 +115,8 @@ defmodule Ash.Schema do
Module.put_attribute(
__MODULE__,
:ash_struct_fields,
{aggregate.name, %Ash.NotLoaded{type: :aggregate, field: aggregate.name}}
{aggregate.name,
%Ash.NotLoaded{type: :aggregate, field: aggregate.name, resource: __MODULE__}}
)
end

Expand Down Expand Up @@ -140,7 +146,8 @@ defmodule Ash.Schema do
Module.put_attribute(
__MODULE__,
:ash_struct_fields,
{calculation.name, %Ash.NotLoaded{type: :calculation, field: calculation.name}}
{calculation.name,
%Ash.NotLoaded{type: :calculation, field: calculation.name, resource: __MODULE__}}
)
end

Expand Down Expand Up @@ -258,7 +265,12 @@ defmodule Ash.Schema do
Module.put_attribute(
__MODULE__,
:ash_struct_fields,
{relationship.name, %Ash.NotLoaded{type: :relationship, field: relationship.name}}
{relationship.name,
%Ash.NotLoaded{
type: :relationship,
field: relationship.name,
resource: __MODULE__
}}
)
end

Expand All @@ -281,7 +293,8 @@ defmodule Ash.Schema do
Module.put_attribute(
__MODULE__,
:ash_struct_fields,
{aggregate.name, %Ash.NotLoaded{type: :aggregate, field: aggregate.name}}
{aggregate.name,
%Ash.NotLoaded{type: :aggregate, field: aggregate.name, resource: __MODULE__}}
)
end

Expand Down Expand Up @@ -311,7 +324,8 @@ defmodule Ash.Schema do
Module.put_attribute(
__MODULE__,
:ash_struct_fields,
{calculation.name, %Ash.NotLoaded{type: :calculation, field: calculation.name}}
{calculation.name,
%Ash.NotLoaded{type: :calculation, field: calculation.name, resource: __MODULE__}}
)
end

Expand Down
127 changes: 127 additions & 0 deletions test/not_loaded_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# SPDX-FileCopyrightText: 2019 ash contributors <https://github.com/ash-project/ash/graphs/contributors>
#
# SPDX-License-Identifier: MIT

defmodule Ash.Test.NotLoadedTest do
@moduledoc false
use ExUnit.Case, async: true

defmodule Domain do
@moduledoc false
use Ash.Domain

resources do
allow_unregistered? true
end
end

defmodule Post do
@moduledoc false
use Ash.Resource,
domain: Domain,
data_layer: Ash.DataLayer.Ets

ets do
private?(true)
end

actions do
default_accept :*
defaults [:read, :destroy, create: :*, update: :*]
end

attributes do
uuid_primary_key :id
attribute :title, :string, public?: true
attribute :body, :string, public?: true
end

relationships do
belongs_to :author, __MODULE__, public?: true
end

calculations do
calculate :title_upcase, :string, expr(string_upcase(title)) do
public? true
end
end

aggregates do
count :count_of_related, :author, public?: true
end
end

describe "struct" do
test "resource field defaults to nil" do
not_loaded = %Ash.NotLoaded{type: :relationship, field: :author}
assert not_loaded.resource == nil
end

test "resource field can be set" do
not_loaded = %Ash.NotLoaded{type: :relationship, field: :author, resource: Post}
assert not_loaded.resource == Post
end
end

describe "resource in struct defaults" do
test "relationship fields have resource set in struct default" do
post = struct(Post)
assert %Ash.NotLoaded{resource: Post, type: :relationship} = post.author
end

test "aggregate fields have resource set in struct default" do
post = struct(Post)
assert %Ash.NotLoaded{resource: Post, type: :aggregate} = post.count_of_related
end

test "calculation fields have resource set in struct default" do
post = struct(Post)
assert %Ash.NotLoaded{resource: Post, type: :calculation} = post.title_upcase
end
end

describe "inspect" do
test "shows resource when inspected standalone" do
not_loaded = %Ash.NotLoaded{type: :relationship, field: :author, resource: Post}
result = inspect(not_loaded)
assert result =~ "resource:"
assert result =~ "Post"
end

test "omits resource when resource is nil" do
not_loaded = %Ash.NotLoaded{type: :relationship, field: :author}
result = inspect(not_loaded)
refute result =~ "resource:"
end

test "hides resource inside a record inspect" do
post = struct(Post)
result = inspect(post)
assert result =~ "NotLoaded"
refute result =~ "resource:"
end

test "hides resource when in_resource? custom option is set" do
not_loaded = %Ash.NotLoaded{type: :relationship, field: :author, resource: Post}
opts = %Inspect.Opts{custom_options: [in_resource?: true]}

result =
Inspect.Algebra.format(Inspect.inspect(not_loaded, opts), 80)
|> IO.iodata_to_binary()

refute result =~ "resource:"
end

test "shows resource when in_resource? is not set" do
not_loaded = %Ash.NotLoaded{type: :relationship, field: :author, resource: Post}
opts = %Inspect.Opts{}

result =
Inspect.Algebra.format(Inspect.inspect(not_loaded, opts), 80)
|> IO.iodata_to_binary()

assert result =~ "resource:"
assert result =~ "Post"
end
end
end
Loading