spectra (spectra v0.17.0)

View Source

Summary

Functions

Decodes data from the specified format into an Erlang term based on type information.

Decodes data from the specified format into an Erlang term based on type information.

Encodes an Erlang term to the specified format based on type information.

Encodes an Erlang term to the specified format based on type information.

Formats the badarg errors raised by encode/5 for an invalid nil_fields option. Called by erl_error and by Elixir to build the error message.

Generates a schema for the specified type in the given format.

Generates a schema for the specified type in the given format.

Types

binary_string_decode_opts()

-type binary_string_decode_opts() :: map().

binary_string_encode_opts()

-type binary_string_encode_opts() :: map().

codec_decode_result()

-type codec_decode_result() :: {ok, dynamic()} | {error, [error()]} | continue.

Return type for codec decode/4 callbacks. See spectra_codec.

codec_encode_result()

-type codec_encode_result() :: {ok, dynamic()} | {error, [error()]} | continue.

Return type for codec encode/4 callbacks. See spectra_codec.

codec_key()

-type codec_key() :: {module(), sp_type_reference()}.

decode_option()

-type decode_option() :: pre_decoded | {pre_decoded, boolean()}.

encode_option()

-type encode_option() :: pre_encoded | {pre_encoded, boolean()} | {nil_fields, omit | null}.

error()

-type error() ::
          #sp_error{location :: [string() | atom() | integer()],
                    type :: decode_error | type_mismatch | no_match | missing_data | not_matched_fields,
                    ctx ::
                        #{type =>
                              spectra:sp_type_or_ref() | spectra:map_field() | spectra:record_field(),
                          value => dynamic(),
                          errors => [{spectra:sp_type(), [#sp_error{}]}],
                          message => binary(),
                          allowed_values => [term()],
                          type_args => [{atom(), spectra:sp_type()}],
                          err_type => atom(),
                          err_reason => term()}}.

function_doc()

-type function_doc() :: #{summary => binary(), description => binary(), deprecated => boolean()}.

literal_value()

-type literal_value() :: integer() | atom() | [].

map_field()

-type map_field() ::
          #literal_map_field{kind :: assoc | exact,
                             name :: atom() | integer(),
                             binary_name :: binary(),
                             val_type :: spectra:sp_type()} |
          #typed_map_field{kind :: assoc | exact,
                           key_type :: spectra:sp_type(),
                           val_type :: spectra:sp_type()}.

missing_value()

-type missing_value() :: undefined | nil.

module_types_cache()

-type module_types_cache() :: persistent | local | none.

nil_fields()

-type nil_fields() :: omit_or_null | omit | null.

How a field whose type can be missing (T | undefined, T | nil) is handled when its value is the missing marker. Set it with {spectra, [{nil_fields, Value}]}.

  • omit_or_null (the default): the schema leaves the field out of required and allows null for it. Encoding leaves the field out, unless the encode/5 call passes {nil_fields, null}, which writes it as null.
  • omit: the schema leaves the field out of required and does not allow null. Encoding leaves the field out. Passing {nil_fields, null} to encode/5 raises badarg.
  • null: the schema lists the field in required and allows null. Encoding writes the field as null. Passing {nil_fields, omit} to encode/5 raises badarg.

A per-call {nil_fields, Value} that equals the setting is always allowed. A whole value whose type can be missing encodes the missing value as null; its schema allows null unless the setting is omit.

record_field()

-type record_field() ::
          #sp_rec_field{name :: atom(),
                        binary_name :: binary(),
                        type :: spectra:sp_type(),
                        default :: undefined | {value, term()}}.

record_field_arg()

-type record_field_arg() :: {FieldName :: atom(), sp_type()}.

schema_option()

-type schema_option() :: pre_encoded | {pre_encoded, boolean()}.

simple_types()

-type simple_types() ::
          string | nonempty_string | integer | non_neg_integer | neg_integer | pos_integer | float |
          number | boolean | binary | nonempty_binary | bitstring | nonempty_bitstring | atom | term |
          reference | pid | port | iolist | iodata | none | map.

sp_config()

-type sp_config() ::
          #sp_config{module_types_cache :: spectra:module_types_cache(),
                     codecs :: #{spectra:codec_key() => module()},
                     nil_fields :: spectra:nil_fields(),
                     missing_value_as_null :: boolean()}.

sp_function_spec()

-type sp_function_spec() ::
          #sp_function_spec{args :: [spectra:sp_type()],
                            return :: spectra:sp_type(),
                            meta :: spectra:sp_function_spec_meta()}.

sp_function_spec_meta()

-type sp_function_spec_meta() :: #{doc => function_doc()}.

sp_type()

-type sp_type() ::
          #sp_simple_type{type :: spectra:simple_types(), meta :: spectra:sp_type_meta()} |
          #sp_rec_ref{record_name :: spectra:user_type_name(),
                      field_types :: [spectra:record_field_arg()],
                      meta :: spectra:sp_type_meta()} |
          #sp_user_type_ref{type_name :: spectra:user_type_name(),
                            variables :: [spectra:sp_type()],
                            arity :: arity(),
                            meta :: spectra:sp_type_meta()} |
          #sp_var{name :: atom(), meta :: spectra:sp_type_meta()} |
          #sp_map{fields :: [spectra:map_field()],
                  struct_name :: undefined | atom(),
                  meta :: spectra:sp_type_meta()} |
          #sp_rec{name :: atom(),
                  fields ::
                      [#sp_rec_field{name :: atom(),
                                     binary_name :: binary(),
                                     type :: spectra:sp_type(),
                                     default :: undefined | {value, term()}}],
                  arity :: pos_integer(),
                  meta :: spectra:sp_type_meta()} |
          #sp_tuple{fields :: any | [spectra:sp_type()], meta :: spectra:sp_type_meta()} |
          #sp_type_with_variables{type :: spectra:sp_type(),
                                  vars :: [atom()],
                                  meta :: spectra:sp_type_meta()} |
          #sp_function{args :: any | [spectra:sp_type()],
                       return :: spectra:sp_type(),
                       meta :: spectra:sp_type_meta()} |
          #sp_union{types :: [spectra:sp_type()], meta :: spectra:sp_type_meta()} |
          #sp_literal{value :: spectra:literal_value(),
                      binary_value :: binary(),
                      meta :: spectra:sp_type_meta()} |
          #sp_range{type :: integer,
                    lower_bound :: integer(),
                    upper_bound :: integer(),
                    meta :: spectra:sp_type_meta()} |
          #sp_list{type :: spectra:sp_type(), meta :: spectra:sp_type_meta()} |
          #sp_nonempty_list{type :: spectra:sp_type(), meta :: spectra:sp_type_meta()} |
          #sp_maybe_improper_list{elements :: spectra:sp_type(),
                                  tail :: spectra:sp_type(),
                                  meta :: spectra:sp_type_meta()} |
          #sp_nonempty_improper_list{elements :: spectra:sp_type(),
                                     tail :: spectra:sp_type(),
                                     meta :: spectra:sp_type_meta()} |
          #sp_remote_type{mfargs :: {module(), atom(), [spectra:sp_type()]},
                          arity :: arity(),
                          meta :: spectra:sp_type_meta()}.

sp_type_meta()

-type sp_type_meta() ::
          #{doc => type_doc(),
            name => sp_type_reference(),
            parameters => term(),
            only => [atom()],
            field_aliases => #{atom() | integer() => binary()}}.

sp_type_or_ref()

-type sp_type_or_ref() :: sp_type() | sp_type_reference().

sp_type_reference()

-type sp_type_reference() :: {type, Name :: atom(), Arity :: arity()} | {record, Name :: atom()}.

type_doc()

-type type_doc() ::
          #{title => binary(),
            description => binary(),
            deprecated => boolean(),
            examples => [dynamic()],
            examples_function => {module(), atom(), [term()]}}.

type_info()

-type type_info() :: spectra_type_info:type_info().

user_type_name()

-type user_type_name() :: atom().

var_type()

-type var_type() :: {VarName :: atom(), sp_type()}.

Functions

decode(Format, ModuleOrTypeinfo, TypeOrRef, Data)

-spec decode(Format :: atom(),
             ModuleOrTypeinfo :: module() | type_info(),
             TypeOrRef :: atom() | sp_type_or_ref(),
             Data :: dynamic()) ->
                {ok, dynamic()} | {error, [error()]}.

Decodes data from the specified format into an Erlang term based on type information.

The function validates the decoded data against the type specification and returns an error if the data doesn't match the expected type.

Example:

-module(my_module).
-type user_id() :: pos_integer().
-type status() :: active | inactive | pending.
-record(user, {id :: user_id(), name :: binary(), age :: integer(), status :: status()}).

1> spectra:decode(json, my_module, user_id, <<"123">>).
{ok, 123}

2> spectra:decode(json, my_module, user, <<"{\"id\":42,\"name\":\"Bob\",\"age\":25, \"status\":\"active\"}">>).
{ok, #user{id = 42, name = <<"Bob">>, age = 25, status = active}}

3> spectra:decode(binary_string, my_module, status, <<"active">>).
{ok, active}

4> spectra:decode(json, my_module, user_id, <<"\"not_a_number\"">>).
{error, [#sp_error{type = type_mismatch, ...}]}

decode(Format, ModuleOrTypeinfo, TypeOrRef, Data, Options)

-spec decode(Format :: atom(),
             ModuleOrTypeinfo :: module() | type_info(),
             TypeOrRef :: atom() | sp_type_or_ref(),
             Data :: dynamic(),
             Options :: [decode_option()]) ->
                {ok, dynamic()} | {error, [error()]}.

Decodes data from the specified format into an Erlang term based on type information.

Accepts an options list. Supported options:

  • pre_decoded: The input is already a decoded term (e.g. a JSON map from a web framework). Skips the deserialization step and passes the value directly to the type decoder.

Example:

1> DecodedJson = #{<<"id">> => 42, <<"name">> => <<"Bob">>}.
2> spectra:decode(json, my_module, user, DecodedJson, [pre_decoded]).
{ok, #user{id = 42, name = <<"Bob">>}}

encode(Format, ModuleOrTypeinfo, TypeOrRef, Data)

-spec encode(Format :: atom(),
             ModuleOrTypeinfo :: module() | type_info(),
             TypeOrRef :: atom() | sp_type_or_ref(),
             Data :: dynamic()) ->
                {ok, dynamic()} | {error, [error()]}.

Encodes an Erlang term to the specified format based on type information.

The function validates the Erlang term against the type specification before encoding and returns an error if the data doesn't match the expected type.

Example:

-module(my_module).
-type user_id() :: pos_integer().
-type status() :: active | inactive | pending.
-record(user, {id :: user_id(), name :: binary(), age :: integer(), status :: status()}).

1> spectra:encode(json, my_module, user_id, 123).
{ok, <<"123">>}

2> User = #user{id = 42, name = <<"Bob">>, age = 25, status = active}.
3> spectra:encode(json, my_module, user, User).
{ok, <<"{\"id\":42,\"name\":\"Bob\",\"age\":25, \"status\":\"active\"}">>}

4> spectra:encode(json, my_module, user_id, -5).
{error, [#sp_error{type = type_mismatch, ...}]}

encode(Format, ModuleOrTypeinfo, TypeOrRef, Data, Options)

-spec encode(Format :: atom(),
             ModuleOrTypeinfo :: module() | type_info(),
             TypeOrRef :: atom() | sp_type_or_ref(),
             Data :: dynamic(),
             Options :: [encode_option()]) ->
                {ok, dynamic()} | {error, [error()]}.

Encodes an Erlang term to the specified format based on type information.

Accepts an options list. Supported options:

  • pre_encoded: Skip the final serialization step and return the intermediate term instead of bytes. For JSON, this returns a map/list/scalar instead of a binary.
  • {nil_fields, null | omit}: How to encode a field whose type can be missing and whose value is the missing marker, for this call only (nested values included). null writes it as JSON null, omit leaves it out. The schema does not change, it follows the nil_fields application setting (see nil_fields/0), so the option must give output that schema accepts. Raises badarg when the value is not null or omit, for null when the setting is omit (the schema does not allow null), and for omit when the setting is null (the schema requires the field). The error's stack frame holds the arity instead of the arguments, so Data is not logged.

Example:

1> spectra:encode(json, my_module, user, #user{id = 42, name = <<"Bob">>}, [pre_encoded]).
{ok, #{<<"id">> => 42, <<"name">> => <<"Bob">>}}

format_error(Reason, StackTrace)

-spec format_error(Reason :: term(), StackTrace :: erlang:stacktrace()) ->
                      #{pos_integer() | general => unicode:chardata()}.

Formats the badarg errors raised by encode/5 for an invalid nil_fields option. Called by erl_error and by Elixir to build the error message.

get_config()

-spec get_config() -> sp_config().

schema(Format, ModuleOrTypeinfo, TypeOrRef)

-spec schema(Format :: atom(),
             ModuleOrTypeinfo :: module() | type_info(),
             TypeOrRef :: atom() | sp_type_or_ref()) ->
                iodata() | dynamic().

Generates a schema for the specified type in the given format.

Equivalent to calling schema/4 with an empty options list.

Example:

-module(my_module).
-type user_id() :: pos_integer().
-type status() :: active | inactive | pending.
-record(user, {id :: user_id(), name :: binary(), age :: integer(), status :: status()}).

1> spectra:schema(json_schema, my_module, user).
<<"{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},...}}">>

2> spectra:schema(json_schema, my_module, status).
<<"{\"type\":\"string\",\"enum\":[\"active\",\"inactive\",\"pending\"]}">>

3> spectra:schema(json_schema, my_module, {type, user_id, 0}).
<<"{\"type\":\"integer\",\"minimum\":1}">>

schema(Format, ModuleOrTypeinfo, TypeOrRef, Options)

-spec schema(Format :: atom(),
             ModuleOrTypeinfo :: module() | type_info(),
             TypeOrRef :: atom() | sp_type_or_ref(),
             Options :: [schema_option()]) ->
                iodata() | dynamic().

Generates a schema for the specified type in the given format.

Accepts an options list. Supported options:

  • pre_encoded: Skip the final JSON encoding step and return the raw schema map instead of encoded iodata(). Useful for inspecting or manipulating the schema before serialisation.

Example:

1> spectra:schema(json_schema, my_module, user, [pre_encoded]).
#{<<"type">> => <<"object">>, <<"properties">> => #{...}}