-module(spectra_codec). -include("../include/spectra_internal.hrl"). -doc """ Behaviour for custom codecs that extend spectra's encoding, decoding, and schema generation for specific types and formats. ## Registering a codec Codecs are registered in the application environment before use: ```erlang application:set_env(spectra, codecs, #{ {my_module, {type, my_type, 0}} => my_codec }). ``` Alternatively, add `-behaviour(spectra_codec)` to the module that *defines* the type and spectra will discover the codec automatically at load time. ## Callback arguments Every callback receives two type-related arguments for inspecting the type in context. ### `CallerTypeInfo :: spectra:type_info()` — caller's module type information `CallerTypeInfo` is the type information of the module where the type traversal is currently taking place. This allows the codec to recursively encode/decode user-defined generic arguments that might be defined locally in that module. ### `TargetType :: spectra:sp_type()` — instantiation node with type-variable bindings `TargetType` is the type node from the traversal at the point where the codec was invoked. For generic types this is the reference node — `#sp_user_type_ref{}` or `#sp_remote_type{}` — and it carries the **concrete type-variable bindings** of that specific instantiation. Use `spectra_type:type_args/1` to extract them. Values supplied via `-spectra(#{type_parameters => ...})` are **not** propagated onto reference nodes (`#sp_user_type_ref{}` / `#sp_remote_type{}`), so `spectra_type:parameters/1` on `TargetType` returns `undefined` during mid-traversal dispatch. It is only reliable when `TargetType` is the resolved type definition (i.e. the codec is invoked from the `spectra:encode/decode/schema` entry points directly). For a type written as `dict:dict(binary(), integer())` the codec receives the `#sp_remote_type{}` node and can extract `[BinaryType, IntegerType]` to recursively encode/decode keys and values: ```erlang encode(json, CallerTypeInfo, _TargetTypeRef, TargetType, Data, Config) -> [KeyType, ValueType] = spectra_type:type_args(TargetType), encode_pairs(CallerTypeInfo, KeyType, ValueType, dict:to_list(Data), #{}, Config). ``` Note: when a codec is invoked from the `spectra:encode/decode/schema` entry points (rather than via mid-traversal dispatch), `TargetType` is the resolved type definition and `spectra_type:type_args/1` returns `[]`. In practice this only affects codecs for generic types — they are naturally called from the traversal where the reference node is available. """. -callback encode( Format :: atom(), CallerTypeInfo :: spectra:type_info(), TargetTypeRef :: spectra:sp_type_reference(), TargetType :: spectra:sp_type(), Data :: dynamic(), Config :: spectra:sp_config() ) -> spectra:codec_encode_result(). -callback decode( Format :: atom(), CallerTypeInfo :: spectra:type_info(), TargetTypeRef :: spectra:sp_type_reference(), TargetType :: spectra:sp_type(), Input :: dynamic(), Config :: spectra:sp_config() ) -> spectra:codec_decode_result(). -callback schema( Format :: atom(), CallerTypeInfo :: spectra:type_info(), TargetTypeRef :: spectra:sp_type_reference(), TargetType :: spectra:sp_type(), Config :: spectra:sp_config() ) -> dynamic(). -optional_callbacks([schema/5]). -export([ try_codec_encode/7, try_codec_decode/7, try_codec_schema/6 ]). -doc "Encodes `Data` via a registered codec for `TargetModule`, or returns `continue`.". -spec try_codec_encode( Format :: atom(), CallerTypeInfo :: spectra:type_info(), TargetModule :: module(), TargetTypeRef :: spectra:sp_type_reference(), TargetType :: spectra:sp_type(), Data :: dynamic(), Config :: spectra:sp_config() ) -> spectra:codec_encode_result(). try_codec_encode(Format, CallerTypeInfo, TargetModule, TargetTypeRef, TargetType, Data, Config) -> case Config#sp_config.codecs of #{{TargetModule, TargetTypeRef} := M} -> M:encode(Format, CallerTypeInfo, TargetTypeRef, TargetType, Data, Config); #{} -> case has_local_codec(CallerTypeInfo, TargetModule, Config) of {ok, M} -> M:encode(Format, CallerTypeInfo, TargetTypeRef, TargetType, Data, Config); error -> continue end end. -doc "Decodes `Data` via a registered codec for `TargetModule`, or returns `continue`.". -spec try_codec_decode( Format :: atom(), CallerTypeInfo :: spectra:type_info(), TargetModule :: module(), TargetTypeRef :: spectra:sp_type_reference(), TargetType :: spectra:sp_type(), Data :: dynamic(), Config :: spectra:sp_config() ) -> spectra:codec_decode_result(). try_codec_decode(Format, CallerTypeInfo, TargetModule, TargetTypeRef, TargetType, Data, Config) -> case Config#sp_config.codecs of #{{TargetModule, TargetTypeRef} := M} -> M:decode(Format, CallerTypeInfo, TargetTypeRef, TargetType, Data, Config); #{} -> case has_local_codec(CallerTypeInfo, TargetModule, Config) of {ok, M} -> M:decode(Format, CallerTypeInfo, TargetTypeRef, TargetType, Data, Config); error -> continue end end. -doc "Returns the schema for `TargetModule` via a registered codec, or `continue`.". -spec try_codec_schema( Format :: atom(), CallerTypeInfo :: spectra:type_info(), TargetModule :: module(), TargetTypeRef :: spectra:sp_type_reference(), TargetType :: spectra:sp_type(), Config :: spectra:sp_config() ) -> dynamic() | continue. try_codec_schema(Format, CallerTypeInfo, TargetModule, TargetTypeRef, TargetType, Config) -> case Config#sp_config.codecs of #{{TargetModule, TargetTypeRef} := M} -> invoke_schema(M, Format, CallerTypeInfo, TargetTypeRef, TargetType, Config); #{} -> case has_local_codec(CallerTypeInfo, TargetModule, Config) of {ok, M} -> invoke_schema(M, Format, CallerTypeInfo, TargetTypeRef, TargetType, Config); error -> continue end end. invoke_schema(M, Format, CallerTypeInfo, TargetTypeRef, TargetType, Config) -> try M:schema(Format, CallerTypeInfo, TargetTypeRef, TargetType, Config) catch error:undef -> erlang:error({schema_not_implemented, M, TargetTypeRef}) end. has_local_codec(CallerTypeInfo, TargetModule, Config) -> case spectra_type_info:get_module(CallerTypeInfo) of TargetModule -> spectra_type_info:find_local_codec(CallerTypeInfo); _ -> TargetTypeInfo = spectra_module_types:get(TargetModule, Config), spectra_type_info:find_local_codec(TargetTypeInfo) end.