@@ -105,6 +105,276 @@ defmodule EctoTypedSchema.TypeMapperTest do
105105 end
106106 end
107107
108+ describe "to_elixir_type/2 - custom type override" do
109+ test "returns custom type when :type option is provided" do
110+ custom = quote ( do: MyApp.Status . t ( ) )
111+ assert TypeMapper . to_elixir_type ( :string , type: custom ) == custom
112+ end
113+ end
114+
115+ describe "to_elixir_type/1 - :any type" do
116+ test "maps :any to term()" do
117+ assert infer_type_string ( :any ) == "term()"
118+ end
119+ end
120+
121+ describe "to_elixir_type/1 - bare :array type" do
122+ test "maps :array to list()" do
123+ assert infer_type_string ( :array ) == "list()"
124+ end
125+ end
126+
127+ describe "to_elixir_type/1 - association types" do
128+ test "belongs_to" do
129+ assoc = % Ecto.Association.BelongsTo {
130+ field: :user ,
131+ owner: MyApp.Post ,
132+ related: MyApp.User ,
133+ owner_key: :user_id ,
134+ related_key: :id ,
135+ queryable: MyApp.User ,
136+ cardinality: :one ,
137+ relationship: :parent ,
138+ on_replace: :raise ,
139+ defaults: [ ] ,
140+ on_cast: nil ,
141+ where: [ ] ,
142+ unique: true ,
143+ ordered: false
144+ }
145+
146+ assert infer_type_string ( { :assoc , assoc } ) ==
147+ "Ecto.Schema.belongs_to(MyApp.User.t())"
148+ end
149+
150+ test "has_one" do
151+ assoc = % Ecto.Association.Has {
152+ field: :profile ,
153+ owner: MyApp.User ,
154+ related: MyApp.Profile ,
155+ owner_key: :id ,
156+ related_key: :user_id ,
157+ queryable: MyApp.Profile ,
158+ cardinality: :one ,
159+ relationship: :child ,
160+ on_delete: :nothing ,
161+ on_replace: :raise ,
162+ defaults: [ ] ,
163+ where: [ ] ,
164+ unique: true ,
165+ ordered: false ,
166+ preload_order: [ ]
167+ }
168+
169+ assert infer_type_string ( { :assoc , assoc } ) ==
170+ "Ecto.Schema.has_one(MyApp.Profile.t())"
171+ end
172+
173+ test "has_many" do
174+ assoc = % Ecto.Association.Has {
175+ field: :posts ,
176+ owner: MyApp.User ,
177+ related: MyApp.Post ,
178+ owner_key: :id ,
179+ related_key: :user_id ,
180+ queryable: MyApp.Post ,
181+ cardinality: :many ,
182+ relationship: :child ,
183+ on_delete: :nothing ,
184+ on_replace: :raise ,
185+ defaults: [ ] ,
186+ where: [ ] ,
187+ unique: true ,
188+ ordered: false ,
189+ preload_order: [ ]
190+ }
191+
192+ assert infer_type_string ( { :assoc , assoc } ) ==
193+ "Ecto.Schema.has_many(MyApp.Post.t())"
194+ end
195+
196+ test "many_to_many" do
197+ assoc = % Ecto.Association.ManyToMany {
198+ field: :tags ,
199+ owner: MyApp.Post ,
200+ related: MyApp.Tag ,
201+ owner_key: :id ,
202+ queryable: MyApp.Tag ,
203+ cardinality: :many ,
204+ relationship: :child ,
205+ on_delete: :nothing ,
206+ on_replace: :raise ,
207+ defaults: [ ] ,
208+ join_keys: [ { :post_id , :id } , { :tag_id , :id } ] ,
209+ join_through: "posts_tags" ,
210+ join_where: [ ] ,
211+ join_defaults: [ ] ,
212+ where: [ ] ,
213+ unique: false ,
214+ ordered: false ,
215+ preload_order: [ ]
216+ }
217+
218+ assert infer_type_string ( { :assoc , assoc } ) ==
219+ "Ecto.Schema.many_to_many(MyApp.Tag.t())"
220+ end
221+ end
222+
223+ describe "to_elixir_type/1 - embed types" do
224+ test "embeds_one" do
225+ embed = % { related: MyApp.Address , cardinality: :one }
226+
227+ assert infer_type_string ( { :embed , embed } ) ==
228+ "Ecto.Schema.embeds_one(MyApp.Address.t())"
229+ end
230+
231+ test "embeds_many" do
232+ embed = % { related: MyApp.Address , cardinality: :many }
233+
234+ assert infer_type_string ( { :embed , embed } ) ==
235+ "Ecto.Schema.embeds_many(MyApp.Address.t())"
236+ end
237+ end
238+
239+ describe "to_elixir_type/1 - parameterized types" do
240+ test "{:parameterized, {Ecto.Enum, params}} with enum_values typed opt" do
241+ params = % { values: [ :active , :inactive ] }
242+
243+ result =
244+ { :parameterized , { Ecto.Enum , params } }
245+ |> TypeMapper . to_elixir_type ( enum_values: [ :on , :off ] )
246+ |> Macro . to_string ( )
247+
248+ assert result == ":on | :off"
249+ end
250+
251+ test "{:parameterized, Ecto.Enum, params} 3-tuple form" do
252+ params = % { values: [ :active , :inactive ] }
253+
254+ result =
255+ { :parameterized , Ecto.Enum , params }
256+ |> TypeMapper . to_elixir_type ( [ ] )
257+ |> Macro . to_string ( )
258+
259+ assert result == ":active | :inactive"
260+ end
261+
262+ test "{:parameterized, Ecto.Enum} bare form falls back to atom()" do
263+ result =
264+ { :parameterized , Ecto.Enum }
265+ |> TypeMapper . to_elixir_type ( [ ] )
266+ |> Macro . to_string ( )
267+
268+ assert result == "atom()"
269+ end
270+
271+ test "{:parameterized, {CustomModule, _}} non-Enum" do
272+ result =
273+ { :parameterized , { MyApp.Money , % { } } }
274+ |> TypeMapper . to_elixir_type ( [ ] )
275+ |> Macro . to_string ( )
276+
277+ assert result == "MyApp.Money.t()"
278+ end
279+
280+ test "{:parameterized, CustomModule} bare module" do
281+ result =
282+ { :parameterized , MyApp.Money }
283+ |> TypeMapper . to_elixir_type ( [ ] )
284+ |> Macro . to_string ( )
285+
286+ assert result == "MyApp.Money.t()"
287+ end
288+
289+ test "{:parameterized, CustomModule, _} 3-tuple non-Enum" do
290+ result =
291+ { :parameterized , MyApp.Money , % { } }
292+ |> TypeMapper . to_elixir_type ( [ ] )
293+ |> Macro . to_string ( )
294+
295+ assert result == "MyApp.Money.t()"
296+ end
297+ end
298+
299+ describe "to_elixir_type/1 - enum value extraction" do
300+ test "extracts values from %{values: [...]}" do
301+ params = % { values: [ :draft , :published ] }
302+
303+ result =
304+ { :parameterized , { Ecto.Enum , params } }
305+ |> TypeMapper . to_elixir_type ( [ ] )
306+ |> Macro . to_string ( )
307+
308+ assert result == ":draft | :published"
309+ end
310+
311+ test "extracts keys from %{mappings: [...]}" do
312+ params = % { mappings: [ admin: "ADMIN" , user: "USER" ] }
313+
314+ result =
315+ { :parameterized , { Ecto.Enum , params } }
316+ |> TypeMapper . to_elixir_type ( [ ] )
317+ |> Macro . to_string ( )
318+
319+ assert result == ":admin | :user"
320+ end
321+
322+ test "falls back to atom() when params have no values or mappings" do
323+ params = % { something_else: true }
324+
325+ result =
326+ { :parameterized , { Ecto.Enum , params } }
327+ |> TypeMapper . to_elixir_type ( [ ] )
328+ |> Macro . to_string ( )
329+
330+ assert result == "atom()"
331+ end
332+
333+ test "single enum value" do
334+ params = % { values: [ :only_one ] }
335+
336+ result =
337+ { :parameterized , { Ecto.Enum , params } }
338+ |> TypeMapper . to_elixir_type ( [ ] )
339+ |> Macro . to_string ( )
340+
341+ assert result == ":only_one"
342+ end
343+
344+ test "extracts values from AST map with :values key" do
345+ params = { :%{} , [ ] , [ values: [ :a , :b ] ] }
346+
347+ result =
348+ { :parameterized , { Ecto.Enum , params } }
349+ |> TypeMapper . to_elixir_type ( [ ] )
350+ |> Macro . to_string ( )
351+
352+ assert result == ":a | :b"
353+ end
354+
355+ test "extracts keys from AST map with :mappings key" do
356+ params = { :%{} , [ ] , [ mappings: [ x: "X" , y: "Y" ] ] }
357+
358+ result =
359+ { :parameterized , { Ecto.Enum , params } }
360+ |> TypeMapper . to_elixir_type ( [ ] )
361+ |> Macro . to_string ( )
362+
363+ assert result == ":x | :y"
364+ end
365+
366+ test "AST map with neither values nor mappings falls back to atom()" do
367+ params = { :%{} , [ ] , [ other: true ] }
368+
369+ result =
370+ { :parameterized , { Ecto.Enum , params } }
371+ |> TypeMapper . to_elixir_type ( [ ] )
372+ |> Macro . to_string ( )
373+
374+ assert result == "atom()"
375+ end
376+ end
377+
108378 defp infer_type_string ( ecto_type ) do
109379 ecto_type
110380 |> TypeMapper . to_elixir_type ( )
0 commit comments