@@ -114,6 +114,82 @@ defmodule JSONAPI.View do
114114 and then use the `[user: {UserView, :include}]` syntax in your `includes` function. This tells
115115 the serializer to *always* include if its loaded.
116116
117+ ## Polymorphic Resources
118+
119+ Polymorphic resources allow you to serialize different types of data with the same view module.
120+ This is useful when you have a collection of resources that share some common attributes but
121+ have different types, fields, or relationships based on the specific data being serialized.
122+
123+ To enable polymorphic resources, set `polymorphic_resource?: true` when using the JSONAPI.View:
124+
125+ defmodule MediaView do
126+ use JSONAPI.View, polymorphic_resource?: true
127+
128+ def polymorphic_type(%{type: "image"}), do: "image"
129+ def polymorphic_type(%{type: "video"}), do: "video"
130+ def polymorphic_type(%{type: "audio"}), do: "audio"
131+
132+ def polymorphic_fields(%{type: "image"}), do: [:id, :url, :width, :height, :alt_text]
133+ def polymorphic_fields(%{type: "video"}), do: [:id, :url, :duration, :thumbnail]
134+ def polymorphic_fields(%{type: "audio"}), do: [:id, :url, :duration, :bitrate]
135+
136+ def polymorphic_relationships(%{type: "image"}), do: [album: AlbumView]
137+ def polymorphic_relationships(%{type: "video"}), do: [playlist: PlaylistView, author: UserView]
138+ def polymorphic_relationships(%{type: "audio"}), do: [album: AlbumView, artist: ArtistView]
139+ end
140+
141+ ### Required Callbacks for Polymorphic Resources
142+
143+ When using polymorphic resources, you must implement these callbacks instead of their non-polymorphic counterparts:
144+
145+ - `polymorphic_type/1` - Returns the JSONAPI type string based on the data
146+ - `polymorphic_fields/1` - Returns the list of fields to serialize based on the data
147+
148+ ### Optional Callbacks for Polymorphic Resources
149+
150+ - `polymorphic_relationships/1` - Returns relationships specific to the data type (defaults to empty list)
151+
152+ ### Example Usage
153+
154+ With the above `MediaView`, you can serialize different media types:
155+
156+ # Image data
157+ image = %{id: 1, type: "image", url: "/image.jpg", width: 800, height: 600, alt_text: "A photo"}
158+ MediaView.show(image, conn)
159+ # => %{data: %{id: "1", type: "image", attributes: %{url: "/image.jpg", width: 800, height: 600, alt_text: "A photo"}}}
160+
161+ # Video data
162+ video = %{id: 2, type: "video", url: "/video.mp4", duration: 120, thumbnail: "/thumb.jpg"}
163+ MediaView.show(video, conn)
164+ # => %{data: %{id: "2", type: "video", attributes: %{url: "/video.mp4", duration: 120, thumbnail: "/thumb.jpg"}}}
165+
166+ ### Custom Field Functions
167+
168+ You can still define custom field functions that work across all polymorphic types:
169+
170+ defmodule MediaView do
171+ use JSONAPI.View, polymorphic_resource?: true
172+
173+ def file_size(data, _conn) do
174+ # Custom logic to calculate file size
175+ calculate_file_size(data.url)
176+ end
177+
178+ def polymorphic_fields(%{type: "image"}), do: [:id, :url, :file_size, :width, :height]
179+ def polymorphic_fields(%{type: "video"}), do: [:id, :url, :file_size, :duration]
180+ # ... other polymorphic implementations
181+ end
182+
183+ ### Notes
184+
185+ - When `polymorphic_resource?: true` is set, the regular `type/0`, `fields/0`, and `relationships/0`
186+ functions are not used and will return default values (nil or empty list)
187+ - The polymorphic callbacks receive the actual data as their first argument, allowing you to
188+ determine the appropriate type, fields, and relationships dynamically
189+ - All other view functionality (links, meta, hidden fields, etc.) works the same way
190+ - **Important**: Polymorphic resources currently do not work for deserializing data from POST
191+ requests yet. They are only supported for serialization (rendering responses)
192+
117193 ## Options
118194 * `:host` (binary) - Allows the `host` to be overridden for generated URLs. Defaults to `host` of the supplied `conn`.
119195
@@ -140,10 +216,13 @@ defmodule JSONAPI.View do
140216 @ type options :: keyword ( )
141217 @ type resource_id :: String . t ( )
142218 @ type resource_type :: String . t ( )
219+ @ type resource_relationships :: [ { atom ( ) , t ( ) | { t ( ) , :include } | { atom ( ) , t ( ) } | { atom ( ) , t ( ) , :include } } ]
220+ @ type resource_fields :: [ field ( ) ]
143221
144222 @ callback attributes ( data ( ) , Conn . t ( ) | nil ) :: map ( )
145223 @ callback id ( data ( ) ) :: resource_id ( ) | nil
146- @ callback fields ( ) :: [ field ( ) ]
224+ @ callback fields ( ) :: resource_fields ( )
225+ @ callback polymorphic_fields ( data ( ) ) :: resource_fields ( )
147226 @ callback get_field ( field ( ) , data ( ) , Conn . t ( ) ) :: any ( )
148227 @ callback hidden ( data ( ) ) :: [ field ( ) ]
149228 @ callback links ( data ( ) , Conn . t ( ) ) :: links ( )
@@ -152,10 +231,10 @@ defmodule JSONAPI.View do
152231 @ callback pagination_links ( data ( ) , Conn . t ( ) , Paginator . page ( ) , Paginator . options ( ) ) ::
153232 Paginator . links ( )
154233 @ callback path ( ) :: String . t ( ) | nil
155- @ callback relationships ( ) :: [
156- { atom ( ) , t ( ) | { t ( ) , :include } | { atom ( ) , t ( ) } | { atom ( ) , t ( ) , :include } }
157- ]
158- @ callback type ( ) :: resource_type ( )
234+ @ callback relationships ( ) :: resource_relationships ( )
235+ @ callback polymorphic_relationships ( data ( ) ) :: resource_relationships ( )
236+ @ callback type ( ) :: resource_type ( ) | nil
237+ @ callback polymorphic_type ( data ( ) ) :: resource_type ( ) | nil
159238 @ callback url_for ( data ( ) , Conn . t ( ) | nil ) :: String . t ( )
160239 @ callback url_for_pagination ( data ( ) , Conn . t ( ) , Paginator . params ( ) ) :: String . t ( )
161240 @ callback url_for_rel ( term ( ) , String . t ( ) , Conn . t ( ) | nil ) :: String . t ( )
@@ -167,7 +246,8 @@ defmodule JSONAPI.View do
167246 { type , opts } = Keyword . pop ( opts , :type )
168247 { namespace , opts } = Keyword . pop ( opts , :namespace )
169248 { path , opts } = Keyword . pop ( opts , :path )
170- { paginator , _opts } = Keyword . pop ( opts , :paginator )
249+ { paginator , opts } = Keyword . pop ( opts , :paginator )
250+ { polymorphic_resource? , _opts } = Keyword . pop ( opts , :polymorphic_resource? , false )
171251
172252 quote do
173253 alias JSONAPI . { Serializer , View }
@@ -178,6 +258,7 @@ defmodule JSONAPI.View do
178258 @ namespace unquote ( namespace )
179259 @ path unquote ( path )
180260 @ paginator unquote ( paginator )
261+ @ polymorphic_resource? unquote ( polymorphic_resource? )
181262
182263 @ impl View
183264 def id ( nil ) , do: nil
@@ -205,8 +286,21 @@ defmodule JSONAPI.View do
205286 end )
206287 end
207288
208- @ impl View
209- def fields , do: raise ( "Need to implement fields/0" )
289+ cond do
290+ ! @ polymorphic_resource? ->
291+ @ impl View
292+ def fields , do: raise ( "Need to implement fields/0" )
293+
294+ @ impl View
295+ def polymorphic_fields ( _data ) , do: [ ]
296+
297+ @ polymorphic_resource? ->
298+ @ impl View
299+ def fields , do: [ ]
300+
301+ @ impl View
302+ def polymorphic_fields ( _data ) , do: raise ( "Need to implement polymorphic_fields/1" )
303+ end
210304
211305 @ impl View
212306 def hidden ( _data ) , do: [ ]
@@ -242,10 +336,29 @@ defmodule JSONAPI.View do
242336 def relationships , do: [ ]
243337
244338 @ impl View
245- if @ resource_type do
246- def type , do: @ resource_type
247- else
248- def type , do: raise ( "Need to implement type/0" )
339+ def polymorphic_relationships ( _data ) , do: [ ]
340+
341+ cond do
342+ @ resource_type ->
343+ @ impl View
344+ def type , do: @ resource_type
345+
346+ @ impl View
347+ def polymorphic_type ( _data ) , do: nil
348+
349+ ! @ polymorphic_resource? ->
350+ @ impl View
351+ def type , do: raise ( "Need to implement type/0" )
352+
353+ @ impl View
354+ def polymorphic_type ( _data ) , do: nil
355+
356+ @ polymorphic_resource? ->
357+ @ impl View
358+ def type , do: nil
359+
360+ @ impl View
361+ def polymorphic_type ( _data ) , do: raise ( "Need to implement polymorphic_type/1" )
249362 end
250363
251364 @ impl View
@@ -264,6 +377,30 @@ defmodule JSONAPI.View do
264377 def visible_fields ( data , conn ) ,
265378 do: View . visible_fields ( __MODULE__ , data , conn )
266379
380+ def resource_fields ( data ) do
381+ if @ polymorphic_resource? do
382+ polymorphic_fields ( data )
383+ else
384+ fields ( )
385+ end
386+ end
387+
388+ def resource_type ( data ) do
389+ if @ polymorphic_resource? do
390+ polymorphic_type ( data )
391+ else
392+ type ( )
393+ end
394+ end
395+
396+ def resource_relationships ( data ) do
397+ if @ polymorphic_resource? do
398+ polymorphic_relationships ( data )
399+ else
400+ relationships ( )
401+ end
402+ end
403+
267404 defoverridable View
268405
269406 def index ( models , conn , _params , meta \\ nil , options \\ [ ] ) ,
@@ -336,11 +473,11 @@ defmodule JSONAPI.View do
336473
337474 @ spec url_for ( t ( ) , term ( ) , Conn . t ( ) | nil ) :: String . t ( )
338475 def url_for ( view , data , nil = _conn ) when is_nil ( data ) or is_list ( data ) ,
339- do: URI . to_string ( % URI { path: Enum . join ( [ view . namespace ( ) , path_for ( view ) ] , "/" ) } )
476+ do: URI . to_string ( % URI { path: Enum . join ( [ view . namespace ( ) , path_for ( view , data ) ] , "/" ) } )
340477
341478 def url_for ( view , data , nil = _conn ) do
342479 URI . to_string ( % URI {
343- path: Enum . join ( [ view . namespace ( ) , path_for ( view ) , view . id ( data ) ] , "/" )
480+ path: Enum . join ( [ view . namespace ( ) , path_for ( view , data ) , view . id ( data ) ] , "/" )
344481 } )
345482 end
346483
@@ -349,7 +486,7 @@ defmodule JSONAPI.View do
349486 scheme: scheme ( conn ) ,
350487 host: host ( conn ) ,
351488 port: port ( conn ) ,
352- path: Enum . join ( [ view . namespace ( ) , path_for ( view ) ] , "/" )
489+ path: Enum . join ( [ view . namespace ( ) , path_for ( view , data ) ] , "/" )
353490 } )
354491 end
355492
@@ -358,7 +495,7 @@ defmodule JSONAPI.View do
358495 scheme: scheme ( conn ) ,
359496 host: host ( conn ) ,
360497 port: port ( conn ) ,
361- path: Enum . join ( [ view . namespace ( ) , path_for ( view ) , view . id ( data ) ] , "/" )
498+ path: Enum . join ( [ view . namespace ( ) , path_for ( view , data ) , view . id ( data ) ] , "/" )
362499 } )
363500 end
364501
@@ -392,8 +529,8 @@ defmodule JSONAPI.View do
392529 def visible_fields ( view , data , conn ) do
393530 all_fields =
394531 view
395- |> requested_fields_for_type ( conn )
396- |> net_fields_for_type ( view . fields ( ) )
532+ |> requested_fields_for_type ( data , conn )
533+ |> net_fields_for_type ( view . resource_fields ( data ) )
397534
398535 hidden_fields = view . hidden ( data )
399536
@@ -420,11 +557,11 @@ defmodule JSONAPI.View do
420557 |> URI . to_string ( )
421558 end
422559
423- defp requested_fields_for_type ( view , % Conn { assigns: % { jsonapi_query: % { fields: fields } } } ) do
424- fields [ view . type ( ) ]
560+ defp requested_fields_for_type ( view , data , % Conn { assigns: % { jsonapi_query: % { fields: fields } } } ) do
561+ fields [ view . resource_type ( data ) ]
425562 end
426563
427- defp requested_fields_for_type ( _view , _conn ) , do: nil
564+ defp requested_fields_for_type ( _view , _data , _conn ) , do: nil
428565
429566 defp host ( % Conn { host: host } ) ,
430567 do: Application . get_env ( :jsonapi , :host , host )
@@ -438,5 +575,5 @@ defmodule JSONAPI.View do
438575 defp scheme ( % Conn { scheme: scheme } ) ,
439576 do: Application . get_env ( :jsonapi , :scheme , to_string ( scheme ) )
440577
441- defp path_for ( view ) , do: view . path ( ) || view . type ( )
578+ defp path_for ( view , data ) , do: view . path ( ) || view . resource_type ( data )
442579end
0 commit comments