Current section
Files
Jump to
Current section
Files
src/core/adk_json.erl
%% @doc Safe conversion of ordinary Erlang values to a JSON value.
%%
%% This module is intentionally small and deterministic. It is used at tool
%% and transport boundaries so internal terms such as pids, references, funs,
%% ports, and arbitrary printable crash data never leak into model prompts or
%% externally visible events.
-module(adk_json).
-export([normalize/1, normalize/2]).
-type path_part() :: binary() | non_neg_integer().
-type error_reason() ::
{unsupported_json_term, [path_part()], atom()}
| {invalid_utf8, [path_part()]}
| {invalid_map_key, [path_part()], atom()}
| {duplicate_map_key, [path_part()], binary()}.
-export_type([error_reason/0]).
-spec normalize(term()) -> {ok, term()} | {error, error_reason()}.
normalize(Value) ->
normalize(Value, []).
-spec normalize(term(), [path_part()]) ->
{ok, term()} | {error, error_reason()}.
normalize(Value, Path) when is_binary(Value) ->
case valid_utf8(Value) of
true -> {ok, Value};
false -> {error, {invalid_utf8, Path}}
end;
normalize(Value, _Path) when is_integer(Value); is_float(Value) ->
{ok, Value};
normalize(true, _Path) -> {ok, true};
normalize(false, _Path) -> {ok, false};
normalize(null, _Path) -> {ok, null};
normalize(undefined, _Path) -> {ok, null};
normalize(Value, Path) when is_atom(Value) ->
normalize(atom_to_binary(Value, utf8), Path);
normalize(Value, Path) when is_map(Value) ->
normalize_map(maps:to_list(Value), Path, #{});
normalize([], _Path) ->
{ok, []};
normalize(Value, Path) when is_list(Value) ->
case printable_unicode(Value) of
{ok, Text} -> {ok, Text};
false -> normalize_list(Value, Path, 0, [])
end;
normalize(Value, Path) when is_tuple(Value) ->
normalize_list(tuple_to_list(Value), Path, 0, []);
normalize(Value, Path) ->
{error, {unsupported_json_term, Path, term_type(Value)}}.
normalize_map([], _Path, Acc) ->
{ok, Acc};
normalize_map([{RawKey, Value} | Rest], Path, Acc) ->
case normalize_key(RawKey, Path) of
{ok, Key} ->
case maps:is_key(Key, Acc) of
true -> {error, {duplicate_map_key, Path, Key}};
false ->
case normalize(Value, Path ++ [Key]) of
{ok, JsonValue} ->
normalize_map(Rest, Path,
Acc#{Key => JsonValue});
{error, _} = Error -> Error
end
end;
{error, _} = Error -> Error
end.
normalize_key(Key, Path) when is_binary(Key) ->
case valid_utf8(Key) of
true -> {ok, Key};
false -> {error, {invalid_utf8, Path}}
end;
normalize_key(Key, _Path) when is_atom(Key) ->
{ok, atom_to_binary(Key, utf8)};
normalize_key(Key, Path) when is_list(Key) ->
case printable_unicode(Key) of
{ok, Text} -> {ok, Text};
false -> {error, {invalid_map_key, Path, term_type(Key)}}
end;
normalize_key(Key, Path) ->
{error, {invalid_map_key, Path, term_type(Key)}}.
normalize_list([], _Path, _Index, Acc) ->
{ok, lists:reverse(Acc)};
normalize_list([Value | Rest], Path, Index, Acc) ->
case normalize(Value, Path ++ [Index]) of
{ok, JsonValue} ->
normalize_list(Rest, Path, Index + 1,
[JsonValue | Acc]);
{error, _} = Error -> Error
end;
normalize_list(_ImproperTail, Path, Index, _Acc) ->
{error, {unsupported_json_term, Path ++ [Index], improper_list}}.
printable_unicode(Value) ->
try io_lib:printable_unicode_list(Value) of
true ->
case unicode:characters_to_binary(Value) of
Text when is_binary(Text) -> {ok, Text};
_ -> false
end;
false -> false
catch
_:_ -> false
end.
valid_utf8(Value) ->
case unicode:characters_to_binary(Value, utf8, utf8) of
Value -> true;
_ -> false
end.
term_type(Value) when is_pid(Value) -> pid;
term_type(Value) when is_reference(Value) -> reference;
term_type(Value) when is_function(Value) -> function;
term_type(Value) when is_port(Value) -> port;
term_type(Value) when is_bitstring(Value) -> bitstring;
term_type(Value) when is_list(Value) -> improper_list;
term_type(_) -> other.