%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% @doc Codec Context to be used during encoding and decoding to keep track %%% of the position in the decoded/encoded data, and all the options. %%% %%% The functions issue_XXX must be used for all codec related errors, %%% and the top function MUST either call finalize/3 or catch all the %%% exceptions and call resolve/3. %%% %%% If a codec error could be ignored resulting in a partial result, %%% the catch_and_continue/6 should be used to optionally support %%% partial results. %%% %%% @end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -module(opcua_codec_context). %%% INCLUDES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -include_lib("kernel/include/logger.hrl"). -include("opcua.hrl"). -include("opcua_internal.hrl"). -include("opcua_codec_context.hrl"). %%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% API Functions -export([new/2]). -export([finalize/3]). -export([resolve/3]). -export([catch_and_continue/6]). -export([export_issues/1]). -export([push/3]). -export([pop/3]). -export([format_path/1]). -export([issue/2]). -export([issue_schema_not_found/2]). -export([issue_encoding_not_supported/2]). %%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -type mode() :: encoding | decoding. -type issue() :: {generic_codec_error, Reason :: term(), Stack :: binary()} | {schema_not_found, NodeId :: opcua:node_id(), Stack :: binary()} | {encoding_not_supported, atom(), Stack :: binary()}. -type issues() :: [] | [issue()]. -type ctx() :: #ctx{}. -type options() :: #{ % Address space to use, opcua_nodeset will be use if not specied. space => opcua_space:state(), % Allows partial decoding. Will returns the encoding/decoding issues as a % third element of the returned tupple. allow_partial => boolean() }. -export_type([mode/0, issue/0, issues/0, ctx/0, options/0]). %%% API FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% new(Mode, Opts) when Mode =:= encoding; Mode =:= decoding -> #ctx{ mode = Mode, space = maps:get(space, Opts, opcua_nodeset), allow_partial = maps:get(allow_partial, Opts, false) }. finalize(#ctx{allow_partial = false, issues = []}, Result, Remaining) -> {Result, Remaining}; finalize(#ctx{allow_partial = false} = Ctx, _Result, _Remaining) -> throw_first_issue(Ctx); finalize(#ctx{allow_partial = true} = Ctx, Result, Remaining) -> {Result, Remaining, opcua_codec_context:export_issues(Ctx)}. resolve(throw, {?MODULE, #ctx{} = Ctx}, _ExStack) -> throw_first_issue(Ctx); resolve(Method, Reason, Stack) -> erlang:raise(Method, Reason, Stack). % Handles and exception and optionally recover and continue with the given % result and data. % If the context is not configured for partial resolution, the exception % will bubble up catch_and_continue(throw, {?MODULE, #ctx{allow_partial = true} = ErrorCtx}, _Stack, #ctx{stack = OrigStack}, Result, Data) -> {Result, Data, ErrorCtx#ctx{stack = OrigStack}}; catch_and_continue(Method, Reason, Stack, _OrigCtx, _Result, _Data) -> erlang:raise(Method, Reason, Stack). export_issues(#ctx{issues = Issues}) -> [{N, R, format_stack(S)} || {N, R, S} <- lists:reverse(Issues)]. % Throws the externally formated version of the first issue from the given context. throw_first_issue(Ctx) -> throw(issue_as_reason(Ctx, first_issue(Ctx))). push(#ctx{stack = Stack} = Ctx, key, Tag) -> Ctx#ctx{stack = [{Tag} | Stack]}; push(#ctx{stack = Stack} = Ctx, field, Tag) -> Ctx#ctx{stack = [Tag | Stack]}. pop(#ctx{stack = [{Tag} | Stack]} = Ctx, key, Tag) -> Ctx#ctx{stack = Stack}; pop(#ctx{stack = [Tag | Stack]} = Ctx, field, Tag) -> Ctx#ctx{stack = Stack}. format_path(#ctx{stack = Stack}) -> format_stack(Stack). -spec issue(ctx(), term()) -> no_return(). issue(Ctx, Reason) -> throw_issue(Ctx, generic_codec_error, Reason). -spec issue_schema_not_found(ctx(), term()) -> no_return(). issue_schema_not_found(Ctx, NodeId) -> throw_issue(Ctx, schema_not_found, NodeId). -spec issue_encoding_not_supported(ctx(), term()) -> no_return(). issue_encoding_not_supported(Ctx, Details) -> throw_issue(Ctx, encoding_not_supported, Details). %%% INTERNAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% add_issue(#ctx{issues = Issues, stack = Stack} = Ctx, Name, Params) -> Ctx#ctx{issues = [{Name, Params, Stack} | Issues]}. throw_issue(Ctx, Name, Params) -> throw({?MODULE, add_issue(Ctx, Name, Params)}). format_stack(undefined) -> <<>>; format_stack(Stack) when is_list(Stack) -> iolist_to_binary([format_stack_item(I) || I <- lists:reverse(Stack)]). format_stack_item({K}) -> [$[, format_stack_value(K), $]]; format_stack_item(V) -> [$., format_stack_value(V)]. format_stack_value(B) when is_binary(B) -> B; format_stack_value(I) when is_integer(I) -> integer_to_binary(I); format_stack_value(A) when is_atom(A) -> atom_to_binary(A); format_stack_value(L) when is_list(L) -> list_to_binary(L). first_issue(#ctx{issues = []}) -> undefined; first_issue(#ctx{issues = Issues}) -> lists:last(Issues). issue_as_reason(#ctx{}, undefined) -> undefined; issue_as_reason(#ctx{mode = decoding}, {generic_codec_error, Reason, Stack}) -> {bad_decoding_error, {Reason, format_stack(Stack)}}; issue_as_reason(#ctx{mode = encoding}, {generic_codec_error, Reason, Stack}) -> {bad_encoding_error, {Reason, format_stack(Stack)}}; issue_as_reason(#ctx{}, {encoding_not_supported, Params, Stack}) -> {bad_encoding_not_supported, {encoding_not_supported, Params, format_stack(Stack)}}; issue_as_reason(#ctx{mode = decoding}, {Name, Params, Stack}) -> {bad_decoding_error, {Name, Params, format_stack(Stack)}}; issue_as_reason(#ctx{mode = encoding}, {Name, Params, Stack}) -> {bad_encoding_error, {Name, Params, format_stack(Stack)}}.