# SPDX-FileCopyrightText: 2025 ash_neo4j contributors # # SPDX-License-Identifier: MIT defmodule AshNeo4j.DataLayer.Cast do @moduledoc "Casting for AshNeo4j.DataLayer" require Logger alias AshNeo4j.DataLayer.TypeClassifier alias AshNeo4j.Util @doc """ Casts an Ash.Resource.Attribute, handles single values and arrays of values. Values may be Elixir native types, Neo4j native types. Returns {:ok, value} | {:error, reason} """ def cast(type, value, constraints \\ []) def cast(_type, nil, _constraints) do {:ok, nil} end def cast({:array, inner_type}, value, constraints) when is_list(value) do item_constraints = TypeClassifier.item_constraints(inner_type, constraints) Enum.reduce_while(value, {:ok, []}, fn item, {:ok, acc} -> case cast(inner_type, item, item_constraints) do {:ok, cast_item} -> {:cont, {:ok, [cast_item | acc]}} {:error, reason} -> {:halt, {:error, reason}} end end) |> case do {:ok, items} -> {:ok, Enum.reverse(items)} error -> error end end def cast(type, value, constraints) do case TypeClassifier.classify(type) do {:ok, :native, _type} -> {:ok, value} {:ok, :ash_base64, ash_type} -> case Util.base64_decode(value) do {:ok, decoded} -> cast_ash_type(ash_type, decoded, constraints) {:error, reason} -> {:error, reason} end {:ok, :ash_json, ash_type} -> case Util.json_decode(value) do {:ok, decoded} -> cast_ash_type(ash_type, decoded, constraints) {:error, reason} -> {:error, reason} end {:ok, :ash_uuid, _ash_type} -> {:ok, value} {:ok, :ash, ash_type} -> cast_ash_type(ash_type, value, constraints) {:ok, :geo, ash_type} -> # AshGeo geometry types: the data layer's read_attribute_property/4 # has already decoded the stored JSON STRING into a %Geo.*{} struct # before we get here. cast_stored is identity for AshGeo on those # structs — no extra cast machinery, just follow the ash typing. cast_ash_type(ash_type, value, constraints) _ -> {:error, "AshNeo4j.DataLayer: cannot cast value #{inspect(value)} of type #{inspect(type)}"} end end defp cast_ash_type(Ash.Type.Function, value, _constraints) do try do [module_function | arity] = String.replace_leading(value, "&", "") |> String.split("/") module_function_splits = String.split(module_function, ".") function = List.last(module_function_splits) module = Module.concat(module_function_splits |> Enum.reverse() |> tl() |> Enum.reverse()) case Code.ensure_loaded(module) do {:module, _} -> {:ok, Function.capture(module, String.to_atom(function), String.to_integer(hd(arity)))} {:error, _} -> {:error, "AshNeo4j.DataLayer: cannot cast #{inspect(value)} as Function: module cannot be loaded"} end rescue e -> {:error, "AshNeo4j.DataLayer: cannot cast #{inspect(value)} as Function: #{Exception.message(e)}"} end end defp cast_ash_type(Ash.Type.Module, value, _constraints) do try do module = String.to_existing_atom(value) case Code.ensure_loaded(module) do {:module, _} -> {:ok, module} {:error, _} -> {:error, "AshNeo4j.DataLayer: cannot cast #{inspect(value)} as Module: module cannot be loaded"} end rescue _ -> {:error, "AshNeo4j.DataLayer: cannot cast #{inspect(value)} as Module: not a known atom"} end end defp cast_ash_type(type, value, constraints) do try do case Ash.Type.cast_stored(type, value, constraints) do {:ok, casted} -> {:ok, casted} {:error, reason} -> {:error, "AshNeo4j.DataLayer: cannot cast #{inspect(value)} as #{inspect(type)}: #{inspect(reason)}"} :error -> {:error, "AshNeo4j.DataLayer: cannot cast #{inspect(value)} as #{inspect(type)}"} end rescue e -> {:error, "AshNeo4j.DataLayer: cannot cast #{inspect(value)} as #{inspect(type)}: #{Exception.message(e)}"} end end end