-module(jchat_utils). -export([now_iso8601/0, generate_id/0, validate_id/1, json_encode/1, json_decode/1, format_error/1, validate_account_id/1, extract_auth_token/1, validate_method_call/1, format_method_response/3, format_error_response/3]). %% Generate ISO8601 timestamp now_iso8601() -> {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:universal_time(), iolist_to_binary(io_lib:format("~4..0w-~2..0w-~2..0wT~2..0w:~2..0w:~2..0wZ", [Year, Month, Day, Hour, Minute, Second])). %% Generate a new ID (UUID v4) generate_id() -> <> = crypto:strong_rand_bytes(16), iolist_to_binary(io_lib:format("~8.16.0b-~4.16.0b-4~3.16.0b-~4.16.0b-~12.16.0b", [U0, U1, U2, ((U3 bsr 60) band 16#03) bor 16#08, U3 band 16#0fffffffffff])). %% Validate ID format (URL-safe base64 characters) validate_id(Id) when is_binary(Id) -> Size = byte_size(Id), case Size >= 1 andalso Size =< 255 of true -> validate_id_chars(Id); false -> false end; validate_id(_) -> false. validate_id_chars(<<>>) -> true; validate_id_chars(<>) -> case is_valid_id_char(C) of true -> validate_id_chars(Rest); false -> false end. is_valid_id_char(C) -> (C >= $A andalso C =< $Z) orelse (C >= $a andalso C =< $z) orelse (C >= $0 andalso C =< $9) orelse C =:= $- orelse C =:= $_. %% JSON encoding/decoding json_encode(Term) -> jsx:encode(Term). json_decode(JSON) -> try {ok, jsx:decode(JSON, [return_maps])} catch _:_ -> {error, invalid_json} end. %% Format errors for JMAP responses (RFC 8620 compliant) format_error(not_found) -> #{type => <<"notFound">>}; format_error(invalid_arguments) -> #{type => <<"invalidArguments">>}; format_error(account_not_found) -> #{type => <<"accountNotFound">>}; format_error(forbidden) -> #{type => <<"forbidden">>}; format_error(invalid_result_reference) -> #{type => <<"invalidResultReference">>}; format_error(anchor_not_found) -> #{type => <<"anchorNotFound">>}; format_error(unsupported_sort) -> #{type => <<"unsupportedSort">>}; format_error(unsupported_filter) -> #{type => <<"unsupportedFilter">>}; format_error(cannot_calculate_changes) -> #{type => <<"cannotCalculateChanges">>}; format_error(too_large) -> #{type => <<"tooLarge">>}; format_error(too_many_changes) -> #{type => <<"tooManyChanges">>}; format_error(rate_limited) -> #{type => <<"rateLimited">>}; format_error(request_too_large) -> #{type => <<"requestTooLarge">>}; format_error(limit_exceeded) -> #{type => <<"limitExceeded">>}; format_error(state_mismatch) -> #{type => <<"stateMismatch">>}; format_error(will_destroy) -> #{type => <<"willDestroy">>}; format_error({invalid_arguments, Description}) -> #{type => <<"invalidArguments">>, description => Description}; format_error({not_found, Description}) -> #{type => <<"notFound">>, description => Description}; format_error({forbidden, Description}) -> #{type => <<"forbidden">>, description => Description}; format_error({server_fail, Description}) -> #{type => <<"serverFail">>, description => Description}; format_error(Error) -> #{type => <<"serverFail">>, description => iolist_to_binary(io_lib:format("~p", [Error]))}. %% JMAP-specific utility functions %% Validate account ID format (as per JMAP spec) validate_account_id(AccountId) when is_binary(AccountId) -> validate_id(AccountId); validate_account_id(_) -> false. %% Extract Bearer token from Authorization header extract_auth_token(undefined) -> {error, no_auth_header}; extract_auth_token(<<"Bearer ", Token/binary>>) -> {ok, Token}; extract_auth_token(_) -> {error, invalid_auth_format}. %% Validate JMAP method call structure validate_method_call([Method, Args, CallId]) when is_binary(Method), is_map(Args), is_binary(CallId) -> case validate_method_name(Method) of true -> ok; false -> {error, invalid_method_name} end; validate_method_call(_) -> {error, invalid_method_call_structure}. validate_method_name(Method) -> case binary:split(Method, <<"/">>) of [Type, Operation] when byte_size(Type) > 0, byte_size(Operation) > 0 -> validate_method_chars(Method); _ -> false end. validate_method_chars(<<>>) -> true; validate_method_chars(<>) -> case is_valid_method_char(C) of true -> validate_method_chars(Rest); false -> false end. is_valid_method_char(C) -> (C >= $A andalso C =< $Z) orelse (C >= $a andalso C =< $z) orelse (C >= $0 andalso C =< $9) orelse C =:= $/ orelse C =:= $_. %% Format successful method response format_method_response(Method, Result, CallId) -> [Method, Result, CallId]. %% Format error method response format_error_response(Error, CallId, Method) -> ErrorMap = format_error(Error), [<<"error">>, ErrorMap#{<<"description">> => <<"Error in method: ", Method/binary>>}, CallId].