Last active
March 28, 2025 06:21
-
-
Save nallwhy/d44f37118941d7b3ed55f9eb07001256 to your computer and use it in GitHub Desktop.
Ash resource dump/load
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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