-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} -> <<<
>/binary, Canonical/binary>>; {error, _} -> <<<
>/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(BPKD)), fun((BPKD) -> 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} -> <<<>/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 -> <<<>/binary, (grid(Buttons, fun dialog_button_cell/1))/binary>> end.