@@ -56,6 +56,13 @@ defmodule AshDynamo.EmbeddedType do
5656 3. Dumping the struct back to a plain map for DynamoDB storage (`dump_to_native/2`)
5757 4. Returning `cast_in_query?: false` to prevent Ash from loading through the type
5858
59+ ## Type Validation
60+
61+ Both `cast_input/2` and `cast_stored/2` validate each field through their respective
62+ `Ash.Type` casting functions. If any field fails validation, the entire cast returns
63+ `:error`. This ensures invalid data is caught on both writes and reads — a corrupted
64+ DynamoDB record will raise an error immediately rather than propagating bad data.
65+
5966 ## ExAws.Dynamo.Encodable
6067
6168 The macro automatically implements the `ExAws.Dynamo.Encodable` protocol for the
@@ -77,7 +84,8 @@ defmodule AshDynamo.EmbeddedType do
7784 alias unquote ( resource ) , as: Resource
7885
7986 @ resource unquote ( resource )
80- @ fields @ resource |> Ash.Resource.Info . attributes ( ) |> Enum . map ( & & 1 . name )
87+ @ attributes Ash.Resource.Info . attributes ( @ resource )
88+ @ fields Enum . map ( @ attributes , & & 1 . name )
8189
8290 @ impl Ash.Type
8391 def storage_type ( _constraints ) , do: :map
@@ -87,12 +95,7 @@ defmodule AshDynamo.EmbeddedType do
8795 def cast_input ( % Resource { } = value , _constraints ) , do: { :ok , value }
8896
8997 def cast_input ( % { } = map , _constraints ) do
90- attrs =
91- Map . new ( @ fields , fn field ->
92- { field , map [ field ] || map [ to_string ( field ) ] }
93- end )
94-
95- { :ok , struct! ( Resource , attrs ) }
98+ cast_fields ( map , & Ash.Type . cast_input / 3 )
9699 end
97100
98101 def cast_input ( _value , _constraints ) , do: :error
@@ -102,12 +105,7 @@ defmodule AshDynamo.EmbeddedType do
102105 def cast_stored ( % Resource { } = value , _constraints ) , do: { :ok , value }
103106
104107 def cast_stored ( % { } = map , _constraints ) do
105- attrs =
106- Map . new ( @ fields , fn field ->
107- { field , map [ field ] || map [ to_string ( field ) ] }
108- end )
109-
110- { :ok , struct! ( Resource , attrs ) }
108+ cast_fields ( map , & Ash.Type . cast_stored / 3 )
111109 end
112110
113111 def cast_stored ( _value , _constraints ) , do: :error
@@ -125,6 +123,27 @@ defmodule AshDynamo.EmbeddedType do
125123 @ impl Ash.Type
126124 def cast_in_query? ( _constraints ) , do: false
127125
126+ defp cast_fields ( map , cast_fn ) do
127+ @ attributes
128+ |> Enum . reduce_while ( % { } , fn attr , acc ->
129+ raw =
130+ case Map . fetch ( map , attr . name ) do
131+ { :ok , value } -> value
132+ :error -> map [ to_string ( attr . name ) ]
133+ end
134+
135+ case cast_fn . ( attr . type , raw , attr . constraints ) do
136+ { :ok , value } -> { :cont , Map . put ( acc , attr . name , value ) }
137+ :error -> { :halt , :error }
138+ { :error , _reason } -> { :halt , :error }
139+ end
140+ end )
141+ |> case do
142+ :error -> :error
143+ attrs -> { :ok , struct! ( Resource , attrs ) }
144+ end
145+ end
146+
128147 unless Enumerable . impl_for ( struct! ( Resource , % { } ) ) do
129148 defimpl ExAws.Dynamo.Encodable , for: Resource do
130149 def encode ( value , options ) do
0 commit comments