Current section

Files

Jump to
telega src telega@testing@render.erl
Raw

src/telega@testing@render.erl

-module(telega@testing@render).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/telega/testing/render.gleam").
-export([canonical_json/1, calls_transcript/1, keyboard_grid/1, formatted_frame/1, window_frame/1]).
-export_type([json_value/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Pure canonicalizers for snapshot testing.\n"
"\n"
" Turns API-call recordings and keyboard markup into stable, human-readable\n"
" strings suitable for snapshot libraries like `birdie`. Nothing here depends\n"
" on a snapshot library — the functions just build strings, so bot authors\n"
" can use them with any testing setup.\n"
"\n"
" ```gleam\n"
" import birdie\n"
" import telega/testing/mock\n"
" import telega/testing/render\n"
"\n"
" let #(client, calls) = mock.message_client()\n"
" // ... drive your bot ...\n"
" mock.get_calls(calls)\n"
" |> render.calls_transcript\n"
" |> birdie.snap(title: \"greeting:start_command\")\n"
" ```\n"
).
-type json_value() :: j_null |
{j_bool, boolean()} |
{j_int, integer()} |
{j_float, float()} |
{j_string, binary()} |
{j_array, list(json_value())} |
{j_object, list({binary(), json_value()})}.
-file("src/telega/testing/render.gleam", 238).
-spec pad(integer()) -> binary().
pad(Level) ->
gleam@string:repeat(<<" "/utf8>>, Level).
-file("src/telega/testing/render.gleam", 206).
-spec render_json(json_value(), integer()) -> binary().
render_json(Value, Level) ->
case Value of
j_null ->
<<"null"/utf8>>;
{j_bool, true} ->
<<"true"/utf8>>;
{j_bool, false} ->
<<"false"/utf8>>;
{j_int, I} ->
erlang:integer_to_binary(I);
{j_float, F} ->
gleam_stdlib:float_to_string(F);
{j_string, S} ->
gleam@json:to_string(gleam@json:string(S));
{j_array, []} ->
<<"[]"/utf8>>;
{j_array, Items} ->
Inner = begin
_pipe = Items,
_pipe@1 = gleam@list:map(
_pipe,
fun(Item) ->
<<(pad(Level + 1))/binary,
(render_json(Item, Level + 1))/binary>>
end
),
gleam@string:join(_pipe@1, <<",\n"/utf8>>)
end,
<<<<<<<<"[\n"/utf8, Inner/binary>>/binary, "\n"/utf8>>/binary,
(pad(Level))/binary>>/binary,
"]"/utf8>>;
{j_object, []} ->
<<"{}"/utf8>>;
{j_object, Fields} ->
Inner@1 = begin
_pipe@2 = Fields,
_pipe@3 = gleam@list:map(
_pipe@2,
fun(Field) ->
<<<<<<(pad(Level + 1))/binary,
(gleam@json:to_string(
gleam@json:string(
erlang:element(1, Field)
)
))/binary>>/binary,
": "/utf8>>/binary,
(render_json(erlang:element(2, Field), Level + 1))/binary>>
end
),
gleam@string:join(_pipe@3, <<",\n"/utf8>>)
end,
<<<<<<<<"{\n"/utf8, Inner@1/binary>>/binary, "\n"/utf8>>/binary,
(pad(Level))/binary>>/binary,
"}"/utf8>>
end.
-file("src/telega/testing/render.gleam", 189).
-spec json_value_decoder() -> gleam@dynamic@decode:decoder(json_value()).
json_value_decoder() ->
gleam@dynamic@decode:recursive(
fun() ->
gleam@dynamic@decode:one_of(
gleam@dynamic@decode:map(
{decoder, fun gleam@dynamic@decode:decode_bool/1},
fun(Field@0) -> {j_bool, Field@0} end
),
[gleam@dynamic@decode:map(
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Field@0) -> {j_int, Field@0} end
),
gleam@dynamic@decode:map(
{decoder, fun gleam@dynamic@decode:decode_float/1},
fun(Field@0) -> {j_float, Field@0} end
),
gleam@dynamic@decode:map(
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Field@0) -> {j_string, Field@0} end
),
gleam@dynamic@decode:map(
gleam@dynamic@decode:list(json_value_decoder()),
fun(Field@0) -> {j_array, Field@0} end
),
gleam@dynamic@decode:map(
gleam@dynamic@decode:dict(
{decoder, fun gleam@dynamic@decode:decode_string/1},
json_value_decoder()
),
fun(Fields) -> _pipe = Fields,
_pipe@1 = maps:to_list(_pipe),
_pipe@2 = gleam@list:sort(
_pipe@1,
fun(A, B) ->
gleam@string:compare(
erlang:element(1, A),
erlang:element(1, B)
)
end
),
{j_object, _pipe@2} end
),
gleam@dynamic@decode:map(
gleam@dynamic@decode:optional(
{decoder, fun gleam@dynamic@decode:decode_bool/1}
),
fun(_) -> j_null end
)]
)
end
).
-file("src/telega/testing/render.gleam", 39).
?DOC(
" Parse a JSON string and re-serialize it canonically: object keys sorted,\n"
" two-space indentation. Produces stable diffs regardless of encoder key\n"
" order. Returns `Error(Nil)` if the input is not valid JSON.\n"
).
-spec canonical_json(binary()) -> {ok, binary()} | {error, nil}.
canonical_json(Input) ->
case gleam@json:parse(Input, json_value_decoder()) of
{ok, Value} ->
{ok, render_json(Value, 0)};
{error, _} ->
{error, nil}
end.
-file("src/telega/testing/render.gleam", 169).
-spec method_from_path(binary()) -> binary().
method_from_path(Path) ->
_pipe = Path,
_pipe@1 = gleam@string:split(_pipe, <<"/"/utf8>>),
_pipe@2 = gleam@list:last(_pipe@1),
_pipe@3 = gleam@option:from_result(_pipe@2),
gleam@option:unwrap(_pipe@3, Path).
-file("src/telega/testing/render.gleam", 49).
?DOC(
" Render a transcript of recorded API calls: one entry per call with the\n"
" method name and the canonical JSON body. The bot token never appears in\n"
" the output. Non-JSON bodies are included verbatim.\n"
).
-spec calls_transcript(list(telega@testing@mock:api_call())) -> binary().
calls_transcript(Calls) ->
_pipe = Calls,
_pipe@1 = gleam@list:index_map(
_pipe,
fun(Call, Index) ->
Method = method_from_path(
erlang:element(8, erlang:element(2, Call))
),
Header = <<<<(erlang:integer_to_binary(Index + 1))/binary,
". "/utf8>>/binary,
Method/binary>>,
case erlang:element(4, erlang:element(2, Call)) of
<<""/utf8>> ->
Header;
Body ->
case canonical_json(Body) of
{ok, Canonical} ->
<<<<Header/binary, "\n"/utf8>>/binary,
Canonical/binary>>;
{error, _} ->
<<<<Header/binary, "\n"/utf8>>/binary, Body/binary>>
end
end
end
),
gleam@string:join(_pipe@1, <<"\n\n"/utf8>>).
-file("src/telega/testing/render.gleam", 149).
-spec button_payload(telega@model@types:inline_keyboard_button()) -> binary().
button_payload(Button) ->
case Button of
{inline_keyboard_button,
_,
_,
_,
_,
{some, Data},
_,
_,
_,
_,
_,
_,
_,
_} ->
Data;
{inline_keyboard_button,
_,
_,
_,
{some, Url},
_,
_,
_,
_,
_,
_,
_,
_,
_} ->
<<"url:"/utf8, Url/binary>>;
{inline_keyboard_button,
_,
_,
_,
_,
_,
{some, Web_app},
_,
_,
_,
_,
_,
_,
_} ->
<<"web_app:"/utf8, (erlang:element(2, Web_app))/binary>>;
{inline_keyboard_button,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
{some, Copy_text},
_,
_} ->
<<"copy:"/utf8, (erlang:element(2, Copy_text))/binary>>;
{inline_keyboard_button,
_,
_,
_,
_,
_,
_,
_,
{some, Query},
_,
_,
_,
_,
_} ->
<<"switch:"/utf8, Query/binary>>;
{inline_keyboard_button,
_,
_,
_,
_,
_,
_,
_,
_,
{some, Query@1},
_,
_,
_,
_} ->
<<"switch_current:"/utf8, Query@1/binary>>;
{inline_keyboard_button,
_,
_,
_,
_,
_,
_,
{some, Login_url},
_,
_,
_,
_,
_,
_} ->
<<"login:"/utf8, (erlang:element(2, Login_url))/binary>>;
{inline_keyboard_button,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
{some, true}} ->
<<"pay"/utf8>>;
{inline_keyboard_button, _, _, _, _, _, _, _, _, _, _, _, {some, _}, _} ->
<<"game"/utf8>>;
_ ->
<<"-"/utf8>>
end.
-file("src/telega/testing/render.gleam", 145).
-spec button_cell(telega@model@types:inline_keyboard_button()) -> binary().
button_cell(Button) ->
<<<<<<<<"[ "/utf8, (erlang:element(2, Button))/binary>>/binary, " ]("/utf8>>/binary,
(button_payload(Button))/binary>>/binary,
")"/utf8>>.
-file("src/telega/testing/render.gleam", 105).
-spec grid(list(list(BPJX)), fun((BPJX) -> binary())) -> binary().
grid(Rows, Cell) ->
_pipe = Rows,
_pipe@3 = gleam@list:map(_pipe, fun(Row) -> _pipe@1 = Row,
_pipe@2 = gleam@list:map(_pipe@1, Cell),
gleam@string:join(_pipe@2, <<" | "/utf8>>) end),
gleam@string:join(_pipe@3, <<"\n"/utf8>>).
-file("src/telega/testing/render.gleam", 72).
?DOC(
" Render an inline keyboard as an ASCII grid, one row per line:\n"
"\n"
" ```text\n"
" [ ○ English ](lang:en) | [ ● Русский ](lang:ru)\n"
" [ Docs ](url:https://example.com)\n"
" ```\n"
).
-spec keyboard_grid(telega@model@types:inline_keyboard_markup()) -> binary().
keyboard_grid(Markup) ->
grid(erlang:element(2, Markup), fun button_cell/1).
-file("src/telega/testing/render.gleam", 78).
?DOC(
" Render formatted text as a \"frame\": the parse mode header followed by the\n"
" rendered text. Snapshot one frame per parse mode to pin down escaping.\n"
).
-spec formatted_frame(telega@format:formatted_text()) -> binary().
formatted_frame(Formatted) ->
{Text, Parse_mode} = telega@format:render(Formatted),
<<<<<<"parse_mode: "/utf8,
(telega@format:parse_mode_to_string(Parse_mode))/binary>>/binary,
"\n---\n"/utf8>>/binary,
Text/binary>>.
-file("src/telega/testing/render.gleam", 133).
-spec dialog_button_cell(telega@dialog@types:dialog_button()) -> binary().
dialog_button_cell(Button) ->
Payload = case Button of
{action_button, _, Action_id} ->
Action_id;
{action_arg_button, _, Action_id@1, Arg} ->
<<<<Action_id@1/binary, ":"/utf8>>/binary, Arg/binary>>;
{url_button, _, Url} ->
<<"url:"/utf8, Url/binary>>;
{web_app_button, _, Url@1} ->
<<"web_app:"/utf8, Url@1/binary>>;
{noop_button, _} ->
<<"noop"/utf8>>
end,
<<<<<<<<"[ "/utf8, (erlang:element(2, Button))/binary>>/binary, " ]("/utf8>>/binary,
Payload/binary>>/binary,
")"/utf8>>.
-file("src/telega/testing/render.gleam", 126).
-spec spoiler_suffix(boolean()) -> binary().
spoiler_suffix(Has_spoiler) ->
case Has_spoiler of
true ->
<<" (spoiler)"/utf8>>;
false ->
<<""/utf8>>
end.
-file("src/telega/testing/render.gleam", 115).
-spec media_cell(telega@dialog@types:dialog_media()) -> binary().
media_cell(Media) ->
case Media of
{photo_media, Media@1, Has_spoiler} ->
<<<<"photo "/utf8, Media@1/binary>>/binary,
(spoiler_suffix(Has_spoiler))/binary>>;
{video_media, Media@2, Has_spoiler@1} ->
<<<<"video "/utf8, Media@2/binary>>/binary,
(spoiler_suffix(Has_spoiler@1))/binary>>;
{animation_media, Media@3} ->
<<"animation "/utf8, Media@3/binary>>;
{document_media, Media@4} ->
<<"document "/utf8, Media@4/binary>>
end.
-file("src/telega/testing/render.gleam", 86).
?DOC(
" Render the full visible \"frame\" of a dialog window: parse mode, media,\n"
" text, and button grid. This is the level-1 dialog snapshot —\n"
" `window.render(state, ctx)` is pure, so no network or actors are involved.\n"
).
-spec window_frame(telega@dialog@types:rendered_window()) -> binary().
window_frame(Window) ->
Frame = case erlang:element(4, Window) of
{some, Media} ->
{Text, Parse_mode} = telega@format:render(erlang:element(2, Window)),
<<<<<<<<<<"parse_mode: "/utf8,
(telega@format:parse_mode_to_string(Parse_mode))/binary>>/binary,
"\nmedia: "/utf8>>/binary,
(media_cell(Media))/binary>>/binary,
"\n---\n"/utf8>>/binary,
Text/binary>>;
none ->
formatted_frame(erlang:element(2, Window))
end,
case erlang:element(3, Window) of
[] ->
Frame;
Buttons ->
<<<<Frame/binary, "\n---\n"/utf8>>/binary,
(grid(Buttons, fun dialog_button_cell/1))/binary>>
end.