Skip to content

Commit 336f377

Browse files
committed
fix: handle Union types in serialization
1 parent 086fcdd commit 336f377

3 files changed

Lines changed: 196 additions & 1 deletion

File tree

lib/ash_json_api/serializer.ex

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,10 +1211,40 @@ defmodule AshJsonApi.Serializer do
12111211
Decimal.to_string(value)
12121212
end
12131213

1214+
def serialize_value(
1215+
%Ash.Union{type: name, value: inner_value},
1216+
Ash.Type.Union,
1217+
constraints,
1218+
domain,
1219+
opts
1220+
) do
1221+
case constraints[:types][name] do
1222+
nil ->
1223+
inner_value
1224+
1225+
config ->
1226+
serialize_value(
1227+
inner_value,
1228+
Ash.Type.get_type(config[:type]),
1229+
config[:constraints] || [],
1230+
domain,
1231+
opts
1232+
)
1233+
end
1234+
end
1235+
12141236
def serialize_value(value, type, constraints, domain, opts) do
12151237
{type, constraints} = flatten_new_type(type, constraints || [])
12161238
opts = [skip_only_primary_key?: false, top_level?: false] |> Keyword.merge(opts)
12171239

1240+
if type == Ash.Type.Union and match?(%Ash.Union{}, value) do
1241+
serialize_value(value, Ash.Type.Union, constraints, domain, opts)
1242+
else
1243+
do_serialize_value(value, type, constraints, domain, opts)
1244+
end
1245+
end
1246+
1247+
defp do_serialize_value(value, type, constraints, domain, opts) do
12181248
with Ash.Type.Struct <- type,
12191249
instance_of when not is_nil(instance_of) <- constraints[:instance_of],
12201250
true <- Ash.Resource.Info.resource?(instance_of) do

test/acceptance/union_test.exs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# SPDX-FileCopyrightText: 2019 ash_json_api contributors <https://github.com/ash-project/ash_json_api/graphs/contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule Test.Acceptance.UnionTest do
6+
use ExUnit.Case, async: true
7+
8+
defmodule HeartRate do
9+
use Ash.Resource,
10+
domain: nil,
11+
data_layer: :embedded
12+
13+
attributes do
14+
attribute(:bpm, :integer, public?: true, allow_nil?: false)
15+
end
16+
17+
actions do
18+
defaults([:read, :destroy, create: :*, update: :*])
19+
end
20+
end
21+
22+
defmodule BloodPressure do
23+
use Ash.Resource,
24+
domain: nil,
25+
data_layer: :embedded
26+
27+
attributes do
28+
attribute(:systolic, :integer, public?: true, allow_nil?: false)
29+
attribute(:diastolic, :integer, public?: true, allow_nil?: false)
30+
end
31+
32+
actions do
33+
defaults([:read, :destroy, create: :*, update: :*])
34+
end
35+
end
36+
37+
defmodule MeasurementValue do
38+
use Ash.Type.NewType,
39+
subtype_of: :union,
40+
constraints: [
41+
types: [
42+
heart_rate: [
43+
type: HeartRate,
44+
tag: :type,
45+
tag_value: "heart_rate"
46+
],
47+
blood_pressure: [
48+
type: BloodPressure,
49+
tag: :type,
50+
tag_value: "blood_pressure"
51+
],
52+
note: [
53+
type: :string
54+
]
55+
]
56+
]
57+
end
58+
59+
defmodule Measurement do
60+
use Ash.Resource,
61+
domain: Test.Acceptance.UnionTest.Domain,
62+
data_layer: Ash.DataLayer.Ets,
63+
extensions: [AshJsonApi.Resource]
64+
65+
ets do
66+
private?(true)
67+
end
68+
69+
json_api do
70+
type "measurement"
71+
72+
routes do
73+
base "/measurements"
74+
get :read
75+
index :read
76+
end
77+
end
78+
79+
attributes do
80+
uuid_primary_key(:id)
81+
attribute(:value, MeasurementValue, public?: true, allow_nil?: false)
82+
attribute(:values, {:array, MeasurementValue}, public?: true, default: [])
83+
end
84+
85+
actions do
86+
default_accept([:value, :values])
87+
defaults([:read, :destroy, create: :*, update: :*])
88+
end
89+
end
90+
91+
defmodule Domain do
92+
use Ash.Domain,
93+
otp_app: :ash_json_api,
94+
extensions: [AshJsonApi.Domain]
95+
96+
json_api do
97+
authorize? false
98+
log_errors? false
99+
end
100+
101+
resources do
102+
resource Measurement
103+
end
104+
end
105+
106+
defmodule Router do
107+
use AshJsonApi.Router, domain: Domain
108+
end
109+
110+
import AshJsonApi.Test
111+
112+
setup do
113+
Application.put_env(:ash_json_api, Domain, json_api: [test_router: Router])
114+
:ok
115+
end
116+
117+
test "renders an embedded resource inside a union without needing Jason.Encoder" do
118+
m =
119+
Measurement
120+
|> Ash.Changeset.for_create(:create, %{
121+
value: %{type: "heart_rate", bpm: 72}
122+
})
123+
|> Ash.create!()
124+
125+
response = Domain |> get("/measurements/#{m.id}", status: 200)
126+
attrs = response.resp_body["data"]["attributes"]
127+
128+
assert attrs["value"] == %{"bpm" => 72}
129+
end
130+
131+
test "renders a primitive value inside a union" do
132+
m =
133+
Measurement
134+
|> Ash.Changeset.for_create(:create, %{
135+
value: "feeling fine"
136+
})
137+
|> Ash.create!()
138+
139+
response = Domain |> get("/measurements/#{m.id}", status: 200)
140+
attrs = response.resp_body["data"]["attributes"]
141+
142+
assert attrs["value"] == "feeling fine"
143+
end
144+
145+
test "renders an array of unions" do
146+
m =
147+
Measurement
148+
|> Ash.Changeset.for_create(:create, %{
149+
value: %{type: "heart_rate", bpm: 60},
150+
values: [
151+
%{type: "heart_rate", bpm: 80},
152+
%{type: "blood_pressure", systolic: 120, diastolic: 80}
153+
]
154+
})
155+
|> Ash.create!()
156+
157+
response = Domain |> get("/measurements/#{m.id}", status: 200)
158+
attrs = response.resp_body["data"]["attributes"]
159+
160+
assert attrs["values"] == [
161+
%{"bpm" => 80},
162+
%{"systolic" => 120, "diastolic" => 80}
163+
]
164+
end
165+
end

test/spec_compliance/fetching_data/pagination/keyset_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ defmodule AshJsonApiTest.FetchingData.Pagination.Keyset do
552552

553553
assert %{"meta" => meta} = conn.resp_body
554554

555-
assert meta == %{"page" => %{"total" => 15, "limit" => 5, "more?" => true}}
555+
assert meta == %{"page" => %{"total" => 15, "limit" => 5}}
556556
end
557557

558558
test "collection total is not present when count is false" do

0 commit comments

Comments
 (0)