Skip to content

Instantly share code, notes, and snippets.

@nallwhy
Last active March 28, 2025 06:21
Show Gist options
  • Save nallwhy/d44f37118941d7b3ed55f9eb07001256 to your computer and use it in GitHub Desktop.
Save nallwhy/d44f37118941d7b3ed55f9eb07001256 to your computer and use it in GitHub Desktop.
Ash resource dump/load
defmodule AshResourceHelper do
@moduledoc """
A helper module for serializing (`dump/2`) and deserializing (`load/2`) Ash resources
into and from plain Elixir maps.
## Purpose
This module provides utility functions for converting Ash structs or lists of structs
into simple maps that can be persisted, for example in a JSON field. It also allows you
to reconstruct Ash structs from these dumped maps.
## Functions
### `dump/2`
Converts an Ash struct or list of structs into a plain map or list of maps. Only
attributes are included by default. To include `calculations` and `relationships`,
they must be explicitly specified using the `additional_fields` parameter.
### `load/2`
Takes a dumped map or list of maps and reconstructs the Ash struct(s) by casting
each value back into its proper type based on the resource definition.
## Example
```elixir
# Assuming you have a resource `User`, `Profile`
user = %User{
id: 1,
name: "Alice",
age: 30,
full_name: "Alice Smith", # a calculation
profile: %Profile{bio: "Hello!"} # a relationship
}
# Dumping only attributes
dumped = dump(user)
# => %{id: 1, name: "Alice", age: 30}
# Dumping with a calculation and relationship
dumped = dump(user, [:full_name, :profile])
# => %{id: 1, name: "Alice", age: 30, full_name: "Alice Smith", profile: %{bio: "Hello!"}}
# Loading back into struct
loaded = AshResourceHelper.load(User, dumped)
# => %User{id: 1, name: "Alice", age: 30, full_name: "Alice Smith", profile: %Profile{bio: "Hello!"}}
"""
require Logger
def dump(list_or_struct, additional_fields \\ [])
def dump(nil, _), do: nil
def dump(%Ash.NotLoaded{}, _), do: nil
def dump(list, additional_fields) when is_list(list) do
list
|> Enum.map(fn struct -> dump(struct, additional_fields) end)
end
def dump(%resource{} = struct, additional_fields) do
attributes = Ash.Resource.Info.attributes(resource)
{_embedded_attributes, normal_attributes} =
attributes
|> Enum.split_with(fn attribute ->
Ash.Resource.Info.resource?(attribute.type)
end)
{calculations, additional_fields} =
additional_fields
|> Enum.reduce({[], []}, fn additional_field, {calculations, additional_fields} ->
{name, children_additional_fields} =
case additional_field do
name when is_atom(name) -> {name, []}
{name, children_additional_fields} -> {name, children_additional_fields}
end
case Ash.Resource.Info.calculation(resource, name) do
nil -> {calculations, [{name, children_additional_fields} | additional_fields]}
calculation -> {[calculation | calculations], additional_fields}
end
end)
result =
(normal_attributes ++ calculations)
|> Enum.reduce(%{}, fn %{name: name, type: type, constraints: constraints}, acc ->
value = struct |> Map.get(name)
{:ok, dumped_value} =
Ash.Type.dump_to_embedded(type, value, constraints)
acc |> Map.put(name, dumped_value)
end)
additional_fields
|> Enum.reduce(result, fn {name, children_additional_fields}, result ->
value = struct |> Map.get(name)
result
|> Map.put(name, dump(value, children_additional_fields))
end)
end
def load(_, nil), do: nil
def load(resource_or_struct, list) when is_list(list) do
list
|> Enum.map(fn dumped -> load(resource_or_struct, dumped) end)
end
def load(resource_or_struct, dumped) when is_map(dumped) do
{resource, orig} =
case resource_or_struct do
struct when is_struct(struct) -> {struct.__struct__, struct}
resource when is_atom(resource) -> {resource, struct(resource)}
end
dumped
|> Enum.reduce(orig, fn {name, value}, result ->
with {:ok, name} <- to_atom_name(name),
{:ok, casted_value} <- cast_value(resource, name, value) do
result |> Map.put(name, casted_value)
else
_ ->
Logger.error(
"Failed to cast {#{inspect(resource)}, #{inspect(name)}, #{inspect(value)}}"
)
result
end
end)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment