-module(squirrel@internal@error). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([to_doc/1]). -export_type([error/0, value_identifier_error/0, pointer/0, pointer_kind/0, printable_error/0, paragraph/0]). -type error() :: {pg_cannot_authenticate, binary(), binary()} | {pg_cannot_send_message, binary()} | {pg_cannot_decode_received_message, binary()} | {pg_cannot_receive_message, binary()} | {pg_cannot_describe_query, binary(), binary(), binary(), binary()} | {cannot_read_file, binary(), simplifile:file_error()} | {cannot_write_to_file, binary(), simplifile:file_error()} | {query_file_has_invalid_name, binary(), gleam@option:option(binary()), value_identifier_error()} | {query_has_invalid_column, binary(), binary(), gleam@option:option(binary()), binary(), integer(), value_identifier_error()} | {query_has_unsupported_type, binary(), binary(), binary(), integer(), binary()} | {cannot_parse_query, binary(), binary(), binary(), integer(), gleam@option:option(binary()), gleam@option:option(pointer()), gleam@option:option(binary())}. -type value_identifier_error() :: doesnt_start_with_lowercase_letter | {contains_invalid_grapheme, integer(), binary()} | is_empty. -type pointer() :: {pointer, pointer_kind(), binary()}. -type pointer_kind() :: {name, binary()} | {byte_index, integer()}. -type printable_error() :: {printable_error, binary(), list(paragraph()), gleam@option:option(binary()), gleam@option:option(binary()), gleam@option:option(binary())}. -type paragraph() :: {simple, binary()} | {code, binary(), binary(), gleam@option:option(pointer()), integer()}. -spec style_file(binary()) -> binary(). style_file(File) -> gleam_community@ansi:underline(File). -spec style_inline_code(binary()) -> binary(). style_inline_code(Code) -> <<<<"`"/utf8, Code/binary>>/binary, "`"/utf8>>. -spec style_link(binary()) -> binary(). style_link(Link) -> gleam_community@ansi:underline(Link). -spec printable_error(binary()) -> printable_error(). printable_error(Title) -> {printable_error, Title, [], none, none, none}. -spec add_paragraph(printable_error(), binary()) -> printable_error(). add_paragraph(Error, String) -> erlang:setelement( 3, Error, lists:append(erlang:element(3, Error), [{simple, String}]) ). -spec add_code_paragraph( printable_error(), binary(), binary(), gleam@option:option(pointer()), integer() ) -> printable_error(). add_code_paragraph(Error, File, Content, Point, Starting_line) -> erlang:setelement( 3, Error, lists:append( erlang:element(3, Error), [{code, File, Content, Point, Starting_line}] ) ). -spec report_bug(printable_error(), binary()) -> printable_error(). report_bug(Error, Report_bug) -> erlang:setelement(4, Error, {some, Report_bug}). -spec hint(printable_error(), binary()) -> printable_error(). hint(Error, Hint) -> erlang:setelement(6, Error, {some, Hint}). -spec maybe_hint(printable_error(), gleam@option:option(binary())) -> printable_error(). maybe_hint(Error, Hint) -> erlang:setelement(6, Error, Hint). -spec call_to_action(printable_error(), binary()) -> printable_error(). call_to_action(Error, Wanted) -> erlang:setelement(5, Error, {some, Wanted}). -spec title_doc(binary()) -> glam@doc:document(). title_doc(Title) -> glam@doc:from_string( gleam_community@ansi:red( <<(gleam_community@ansi:bold(<<"Error: "/utf8>>))/binary, Title/binary>> ) ). -spec find_name_span(binary(), integer(), binary(), integer(), integer()) -> {ok, {integer(), integer(), integer()}} | {error, nil}. find_name_span(Name, Name_len, String, Row, Col) -> case gleam@string:starts_with(String, Name) of true -> {ok, {Row, Col, (Col + Name_len) - 1}}; false -> case gleam@string:pop_grapheme(String) of {ok, {<<"\n"/utf8>>, Rest}} -> find_name_span(Name, Name_len, Rest, Row + 1, 0); {ok, {_, Rest@1}} -> find_name_span(Name, Name_len, Rest@1, Row, Col + 1); {error, _} -> {error, nil} end end. -spec find_byte_span(integer(), binary(), integer(), integer()) -> {ok, {integer(), integer(), integer()}} | {error, nil}. find_byte_span(Position, String, Row, Col) -> case Position of 0 -> {ok, {Row, Col, Col}}; N -> case gleam@string:pop_grapheme(String) of {ok, {<<"\n"/utf8>>, Rest}} -> find_byte_span(N - 1, Rest, Row + 1, 0); {ok, {_, Rest@1}} -> find_byte_span(N - 1, Rest@1, Row, Col + 1); {error, _} -> {error, nil} end end. -spec find_span(pointer_kind(), binary()) -> {ok, {integer(), integer(), integer()}} | {error, nil}. find_span(Kind, String) -> case Kind of {name, Name} -> find_name_span(Name, gleam@string:length(Name), String, 0, 0); {byte_index, N} -> find_byte_span(N - 1, String, 0, 0) end. -spec flexible_string(binary()) -> glam@doc:document(). flexible_string(String) -> _pipe = gleam@string:split(String, <<"\n"/utf8>>), _pipe@1 = gleam@list:flat_map( _pipe, fun(_capture) -> gleam@string:split(_capture, <<" "/utf8>>) end ), _pipe@2 = gleam@list:map(_pipe@1, fun glam@doc:from_string/1), _pipe@3 = glam@doc:join(_pipe@2, {flex_break, <<" "/utf8>>, <<""/utf8>>}), glam@doc:group(_pipe@3). -spec pointer_doc(pointer(), binary()) -> {ok, {integer(), integer(), glam@doc:document()}} | {error, nil}. pointer_doc(Pointer, Content) -> {pointer, Kind, Message} = Pointer, gleam@result:'try'( find_span(Kind, Content), fun(_use0) -> {Line, From, To} = _use0, Width = (To - From) + 1, Doc = begin _pipe@1 = [glam@doc:zero_width_string(<<"\x{001B}[31m"/utf8>>), glam@doc:from_string( <<"┬"/utf8, (gleam@string:repeat(<<"─"/utf8>>, Width - 1))/binary>> ), {line, 1}, glam@doc:from_string(<<"╰─ "/utf8>>), begin _pipe = flexible_string(Message), glam@doc:nest(_pipe, 3) end, glam@doc:zero_width_string(<<"\x{001B}[0m"/utf8>>)], _pipe@2 = glam@doc:concat(_pipe@1), glam@doc:group(_pipe@2) end, {ok, {Line, From, Doc}} end ). -spec call_to_action_doc(binary()) -> glam@doc:document(). call_to_action_doc(Wanted) -> flexible_string( <<<<<<"If you would like for "/utf8, Wanted/binary>>/binary, ", please open an issue at "/utf8>>/binary, (style_link( <<"https://github.com/giacomocavalieri/squirrel/issues/new"/utf8>> ))/binary>> ). -spec hint_doc(binary()) -> glam@doc:document(). hint_doc(Hint) -> flexible_string(<<"Hint: "/utf8, Hint/binary>>). -spec option_to_doc(gleam@option:option(QEG), fun((QEG) -> glam@doc:document())) -> glam@doc:document(). option_to_doc(Option, Fun) -> case Option of {some, A} -> Fun(A); none -> {concat, []} end. -spec report_bug_doc(binary()) -> glam@doc:document(). report_bug_doc(Additional_info) -> _pipe@1 = [flexible_string( <<<<"Please open an issue at "/utf8, (style_link( <<"https://github.com/giacomocavalieri/squirrel/issues/new"/utf8>> ))/binary>>/binary, " with some details about what you where doing, including the following message:"/utf8>> ), begin _pipe = {line, 1}, glam@doc:nest(_pipe, 2) end, glam@doc:from_string(Additional_info)], _pipe@2 = glam@doc:concat(_pipe@1), glam@doc:group(_pipe@2). -spec syntax_highlight(binary()) -> binary(). syntax_highlight(Content) -> Keywords = gleam@string:join( [<<"and"/utf8>>, <<"any"/utf8>>, <<"as"/utf8>>, <<"asc"/utf8>>, <<"begin"/utf8>>, <<"between"/utf8>>, <<"by"/utf8>>, <<"case"/utf8>>, <<"count"/utf8>>, <<"desc"/utf8>>, <<"distinct"/utf8>>, <<"else"/utf8>>, <<"end"/utf8>>, <<"exists"/utf8>>, <<"from"/utf8>>, <<"full"/utf8>>, <<"group"/utf8>>, <<"having"/utf8>>, <<"if"/utf8>>, <<"in"/utf8>>, <<"inner"/utf8>>, <<"insert"/utf8>>, <<"into"/utf8>>, <<"join"/utf8>>, <<"key"/utf8>>, <<"left"/utf8>>, <<"like"/utf8>>, <<"not"/utf8>>, <<"null"/utf8>>, <<"on"/utf8>>, <<"or"/utf8>>, <<"order"/utf8>>, <<"primary"/utf8>>, <<"revert"/utf8>>, <<"right"/utf8>>, <<"select"/utf8>>, <<"set"/utf8>>, <<"table"/utf8>>, <<"top"/utf8>>, <<"trigger"/utf8>>, <<"union"/utf8>>, <<"update"/utf8>>, <<"use"/utf8>>, <<"values"/utf8>>, <<"view"/utf8>>, <<"where"/utf8>>, <<"with"/utf8>>], <<"|"/utf8>> ), Not_inside_string = <<"(?=(?:[^']*'[^']*')*[^']*$)"/utf8>>, _assert_subject = begin _pipe = (<<<<<<"\\b("/utf8, Keywords/binary>>/binary, ")\\b"/utf8>>/binary, Not_inside_string/binary>>), gleam@regex:compile(_pipe, {options, true, false}) end, {ok, Keyword} = case _assert_subject of {ok, _} -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"squirrel/internal/error"/utf8>>, function => <<"syntax_highlight"/utf8>>, line => 581}) end, _assert_subject@1 = gleam@regex:from_string( <<"(?> ), {ok, Number} = case _assert_subject@1 of {ok, _} -> _assert_subject@1; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@1, module => <<"squirrel/internal/error"/utf8>>, function => <<"syntax_highlight"/utf8>>, line => 584}) end, _assert_subject@2 = gleam@regex:from_string(<<"(^\\s*--.*)"/utf8>>), {ok, Comment} = case _assert_subject@2 of {ok, _} -> _assert_subject@2; _assert_fail@2 -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@2, module => <<"squirrel/internal/error"/utf8>>, function => <<"syntax_highlight"/utf8>>, line => 586}) end, _assert_subject@3 = gleam@regex:from_string(<<"(\\'.*\\')"/utf8>>), {ok, String} = case _assert_subject@3 of {ok, _} -> _assert_subject@3; _assert_fail@3 -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@3, module => <<"squirrel/internal/error"/utf8>>, function => <<"syntax_highlight"/utf8>>, line => 587}) end, _assert_subject@4 = gleam@regex:from_string( <<"(\\$\\d+)"/utf8, Not_inside_string/binary>> ), {ok, Hole} = case _assert_subject@4 of {ok, _} -> _assert_subject@4; _assert_fail@4 -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@4, module => <<"squirrel/internal/error"/utf8>>, function => <<"syntax_highlight"/utf8>>, line => 588}) end, _pipe@1 = Content, _pipe@2 = gleam_stdlib:regex_replace( Comment, _pipe@1, <<"\x{001B}[2m\\1\x{001B}[0m"/utf8>> ), _pipe@3 = gleam_stdlib:regex_replace( Keyword, _pipe@2, <<"\x{001B}[36m\\1\x{001B}[39m"/utf8>> ), _pipe@4 = gleam_stdlib:regex_replace( String, _pipe@3, <<"\x{001B}[33m\\1\x{001B}[39m"/utf8>> ), _pipe@5 = gleam_stdlib:regex_replace( Number, _pipe@4, <<"\x{001B}[32m\\1\x{001B}[39m"/utf8>> ), gleam_stdlib:regex_replace( Hole, _pipe@5, <<"\x{001B}[35m\\1\x{001B}[39m"/utf8>> ). -spec code_doc(binary(), binary(), gleam@option:option(pointer()), integer()) -> glam@doc:document(). code_doc(File, Content, Pointer, Starting_line) -> Pointer@1 = begin _pipe = gleam@option:to_result(Pointer, nil), gleam@result:then( _pipe, fun(_capture) -> pointer_doc(_capture, Content) end ) end, Content@1 = syntax_highlight(Content), Lines = gleam@string:split(Content@1, <<"\n"/utf8>>), Lines_count = erlang:length(Lines), _assert_subject = gleam@int:digits(Lines_count + Starting_line, 10), {ok, Digits} = case _assert_subject of {ok, _} -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"squirrel/internal/error"/utf8>>, function => <<"code_doc"/utf8>>, line => 463}) end, Max_digits = erlang:length(Digits), Code_lines = (gleam@list:index_map( Lines, fun(Line, I) -> Prefix = begin _pipe@1 = gleam@int:to_string(I + Starting_line), gleam@string:pad_left(_pipe@1, Max_digits + 2, <<" "/utf8>>) end, _pipe@4 = case Pointer@1 of {ok, {Pointer_line, From, Pointer_doc}} when Pointer_line =:= I -> [glam@doc:from_string( gleam_community@ansi:dim( <> ) ), glam@doc:from_string(Line), begin _pipe@2 = [{line, 1}, Pointer_doc], _pipe@3 = glam@doc:concat(_pipe@2), glam@doc:nest(_pipe@3, (From + Max_digits) + 5) end]; {ok, _} -> [glam@doc:from_string( gleam_community@ansi:dim( <> ) ), glam@doc:from_string(gleam_community@ansi:dim(Line))]; {error, _} -> [glam@doc:from_string( gleam_community@ansi:dim( <> ) ), glam@doc:from_string(gleam_community@ansi:dim(Line))] end, glam@doc:concat(_pipe@4) end )), Padding = gleam@string:repeat(<<" "/utf8>>, Max_digits + 3), _pipe@5 = [glam@doc:from_string( <>))/binary>> ), case Starting_line of 1 -> glam@doc:from_string( <>))/binary>> ); _ -> glam@doc:from_string( <>))/binary>> ) end | Code_lines], _pipe@6 = glam@doc:join(_pipe@5, {line, 1}), _pipe@7 = glam@doc:append(_pipe@6, {line, 1}), _pipe@8 = glam@doc:append( _pipe@7, glam@doc:from_string( <>))/binary>> ) ), glam@doc:group(_pipe@8). -spec paragraph_doc(paragraph()) -> glam@doc:document(). paragraph_doc(Paragraph) -> case Paragraph of {simple, String} -> flexible_string(String); {code, File, Content, Pointer, Starting_line} -> code_doc(File, Content, Pointer, Starting_line) end. -spec body_doc(list(paragraph())) -> glam@doc:document(). body_doc(Body) -> _pipe = gleam@list:map(Body, fun paragraph_doc/1), _pipe@1 = glam@doc:join(_pipe, {line, 1}), glam@doc:group(_pipe@1). -spec printable_error_to_doc(printable_error()) -> glam@doc:document(). printable_error_to_doc(Error) -> {printable_error, Title, Body, Report_bug, Call_to_action, Hint} = Error, _pipe = [title_doc(Title), body_doc(Body), option_to_doc(Report_bug, fun report_bug_doc/1), option_to_doc(Call_to_action, fun call_to_action_doc/1), option_to_doc(Hint, fun hint_doc/1)], _pipe@1 = gleam@list:filter(_pipe, fun(Doc) -> Doc /= {concat, []} end), _pipe@2 = glam@doc:join(_pipe@1, glam@doc:lines(2)), glam@doc:group(_pipe@2). -spec to_doc(error()) -> glam@doc:document(). to_doc(Error) -> Printable_error = case Error of {pg_cannot_send_message, Reason} -> _pipe = printable_error(<<"Cannot send message"/utf8>>), _pipe@1 = add_paragraph( _pipe, <<"I ran into an unexpected error while trying to talk to the Postgres database server."/utf8>> ), report_bug(_pipe@1, Reason); {pg_cannot_decode_received_message, Reason@1} -> _pipe@2 = printable_error(<<"Cannot decode message"/utf8>>), _pipe@3 = add_paragraph( _pipe@2, <<"I ran into an unexpected error while trying to decode a message received from the Postgres database server."/utf8>> ), report_bug(_pipe@3, Reason@1); {pg_cannot_receive_message, Reason@2} -> _pipe@4 = printable_error(<<"Cannot receive message"/utf8>>), _pipe@5 = add_paragraph( _pipe@4, <<"I ran into an unexpected error while trying to listen to the Postgres database server."/utf8>> ), report_bug(_pipe@5, Reason@2); {cannot_read_file, File, Reason@3} -> _pipe@6 = printable_error(<<"Cannot read file"/utf8>>), add_paragraph( _pipe@6, <<<<<<"I couldn't read "/utf8, (style_file(File))/binary>>/binary, " because of the following error: "/utf8>>/binary, (simplifile:describe_error(Reason@3))/binary>> ); {cannot_write_to_file, File@1, Reason@4} -> _pipe@7 = printable_error(<<"Cannot write to file"/utf8>>), add_paragraph( _pipe@7, <<<<<<"I couldn't write to "/utf8, (style_file(File@1))/binary>>/binary, " because of the following error: "/utf8>>/binary, (simplifile:describe_error(Reason@4))/binary>> ); {query_file_has_invalid_name, File@2, Suggested_name, _} -> _pipe@8 = printable_error(<<"Query file with invalid name"/utf8>>), _pipe@9 = add_paragraph( _pipe@8, <<<<"File "/utf8, (style_file(File@2))/binary>>/binary, " doesn't have a valid name. The name of a file is used to generate a corresponding Gleam function, so it should be a valid Gleam name."/utf8>> ), hint( _pipe@9, <<"A file name must start with a lowercase letter and can only contain lowercase letters, numbers and underscores."/utf8, (case Suggested_name of {some, Name} -> <<<<"\nMaybe try renaming it to "/utf8, (style_inline_code(Name))/binary>>/binary, "?"/utf8>>; none -> <<""/utf8>> end)/binary>> ); {query_has_invalid_column, File@3, Column_name, Suggested_name@1, Content, Starting_line, Reason@5} -> case Reason@5 of is_empty -> _pipe@10 = printable_error( <<"Column with empty name"/utf8>> ), _pipe@11 = add_code_paragraph( _pipe@10, File@3, Content, none, Starting_line ), add_paragraph( _pipe@11, <<"A column returned by this query has the empty string as a name, all columns should have a valid Gleam name as name."/utf8>> ); _ -> _pipe@12 = printable_error( <<"Column with invalid name"/utf8>> ), _pipe@13 = add_code_paragraph( _pipe@12, File@3, Content, {some, {pointer, {name, Column_name}, case Suggested_name@1 of none -> <<"This is not a valid Gleam name"/utf8>>; {some, Suggestion} -> <<<<"This is not a valid Gleam name, maybe try "/utf8, (style_inline_code(Suggestion))/binary>>/binary, "?"/utf8>> end}}, Starting_line ), hint( _pipe@13, <<"A column name must start with a lowercase letter and can only contain lowercase letters, numbers and underscores."/utf8>> ) end; {query_has_unsupported_type, File@4, _, Content@1, Starting_line@1, Type_} -> _pipe@14 = printable_error(<<"Unsupported type"/utf8>>), _pipe@15 = add_code_paragraph( _pipe@14, File@4, Content@1, none, Starting_line@1 ), _pipe@16 = add_paragraph( _pipe@15, <<<<"One of the rows returned by this query has type "/utf8, (style_inline_code(Type_))/binary>>/binary, " which I cannot currently generate code for."/utf8>> ), call_to_action(_pipe@16, <<"this type to be supported"/utf8>>); {cannot_parse_query, File@5, _, Content@2, Starting_line@2, Error_code, Pointer, Hint} -> _pipe@17 = printable_error(case Error_code of {some, Code} -> <<<<"Invalid query ["/utf8, Code/binary>>/binary, "]"/utf8>>; none -> <<"Invalid query"/utf8>> end), _pipe@18 = add_code_paragraph( _pipe@17, File@5, Content@2, Pointer, Starting_line@2 ), maybe_hint(_pipe@18, Hint); {pg_cannot_authenticate, Expected, Got} -> _pipe@19 = printable_error(<<"Cannot authenticate"/utf8>>), _pipe@20 = add_paragraph( _pipe@19, <<"I ran into an unexpected problem while trying to authenticate with the Postgres server. This is most definitely a bug!"/utf8>> ), report_bug( _pipe@20, <<<<<<"Expected: "/utf8, Expected/binary>>/binary, ", Got: "/utf8>>/binary, Got/binary>> ); {pg_cannot_describe_query, File@6, Query_name, Expected@1, Got@1} -> _pipe@21 = printable_error(<<"Cannot inspect query"/utf8>>), _pipe@22 = add_paragraph( _pipe@21, <<<<<<<<"I ran into an unexpected problem while trying to figure out the types of query "/utf8, (style_inline_code(Query_name))/binary>>/binary, " defined in "/utf8>>/binary, (style_file(File@6))/binary>>/binary, ". This is most definitely a bug!"/utf8>> ), report_bug( _pipe@22, <<<<<<"Expected: "/utf8, Expected@1/binary>>/binary, ", Got: "/utf8>>/binary, Got@1/binary>> ) end, printable_error_to_doc(Printable_error).