-module(squirrel). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/squirrel.gleam"). -export([compare_code_snippets/2, main/0]). -export_type([mode/0, check_result/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. -type mode() :: generate_code | check_generated_code. -type check_result() :: different | same. -file("src/squirrel.gleam", 118). -spec parse_cli_args() -> {ok, mode()} | {error, nil}. parse_cli_args() -> case erlang:element(4, argv:load()) of [] -> {ok, generate_code}; [<<"check"/utf8>>] -> {ok, check_generated_code}; _ -> {error, nil} end. -file("src/squirrel.gleam", 247). -spec check_scheme(gleam@option:option(binary())) -> {ok, nil} | {error, nil}. check_scheme(Scheme) -> case Scheme of {some, <<"postgres"/utf8>>} -> {ok, nil}; {some, <<"postgresql"/utf8>>} -> {ok, nil}; none -> {ok, nil}; {some, _} -> {error, nil} end. -file("src/squirrel.gleam", 254). -spec parse_user_and_password_from_userinfo(gleam@option:option(binary())) -> {gleam@option:option(binary()), gleam@option:option(binary())}. parse_user_and_password_from_userinfo(Userinfo) -> case Userinfo of none -> {none, none}; {some, Userinfo@1} -> case gleam@string:split(Userinfo@1, <<":"/utf8>>) of [User] -> {{some, User}, none}; [User@1, Password | _] -> {{some, User@1}, {some, Password}}; _ -> {none, none} end end. -file("src/squirrel.gleam", 268). -spec parse_database_from_path(binary()) -> gleam@option:option(binary()). parse_database_from_path(Path) -> case gleam@string:split(Path, <<"/"/utf8>>) of [<<""/utf8>>, Database | _] -> {some, Database}; _ -> none end. -file("src/squirrel.gleam", 278). ?DOC( " Finds all `from/**/sql` directories and lists the full paths of the `*.sql`\n" " files inside each one.\n" ). -spec walk(binary()) -> gleam@dict:dict(binary(), list(binary())). walk(From) -> case filepath:base_name(From) of <<"sql"/utf8>> -> Files@1 = case simplifile_erl:read_directory(From) of {ok, Files} -> Files; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"squirrel"/utf8>>, function => <<"walk"/utf8>>, line => 281, value => _assert_fail, start => 8393, 'end' => 8447, pattern_start => 8404, pattern_end => 8413}) end, Files@2 = begin gleam@list:filter_map( Files@1, fun(File) -> gleam@result:'try'( filepath:extension(File), fun(Extension) -> gleam@bool:guard( Extension /= <<"sql"/utf8>>, {error, nil}, fun() -> File_name = filepath:join(From, File), case simplifile_erl:is_file(File_name) of {ok, true} -> {ok, File_name}; {ok, false} -> {error, nil}; {error, _} -> {error, nil} end end ) end ) end ) end, maps:from_list([{From, Files@2}]); _ -> Files@4 = case simplifile_erl:read_directory(From) of {ok, Files@3} -> Files@3; {error, enoent} -> []; {error, _} -> erlang:error(#{gleam_error => panic, message => (<<"couldn't read directory: "/utf8, From/binary>>), file => <>, module => <<"squirrel"/utf8>>, function => <<"walk"/utf8>>, line => 299}) end, Directories = begin gleam@list:filter_map( Files@4, fun(File@1) -> File_name@1 = filepath:join(From, File@1), case simplifile_erl:is_directory(File_name@1) of {ok, true} -> {ok, File_name@1}; {ok, false} -> {error, nil}; {error, _} -> {error, nil} end end ) end, _pipe = gleam@list:map(Directories, fun walk/1), gleam@list:fold(_pipe, maps:new(), fun maps:merge/2) end. -file("src/squirrel.gleam", 322). ?DOC( " Given a dict of directories and their `*.sql` files, performs code\n" " generation for each one, bundling all `*.sql` files under the same directory\n" " into a single Gleam module.\n" ). -spec generate_queries( gleam@dict:dict(binary(), list(binary())), squirrel@internal@database@postgres:connection_options() ) -> gleam@dict:dict(binary(), {list(squirrel@internal@query:typed_query()), list(squirrel@internal@error:error())}). generate_queries(Directories, Connection) -> gleam@dict:map_values( Directories, fun(_, Files) -> {Queries, Errors} = begin _pipe = gleam@list:map( Files, fun squirrel@internal@query:from_file/1 ), gleam@result:partition(_pipe) end, case squirrel@internal@database@postgres:main(Queries, Connection) of {error, Error} -> {[], [Error | Errors]}; {ok, {Queries@1, Type_errors}} -> {Queries@1, lists:append(Errors, Type_errors)} end end ). -file("src/squirrel.gleam", 371). -spec directory_to_output_file(binary()) -> binary(). directory_to_output_file(Directory) -> _pipe = filepath:directory_name(Directory), filepath:join(_pipe, <<"sql.gleam"/utf8>>). -file("src/squirrel.gleam", 444). -spec compare_token_lists( list({glexer@token:token(), glexer:position()}), list({glexer@token:token(), glexer:position()}) ) -> check_result(). compare_token_lists(Expected_tokens, Actual_tokens) -> case {Expected_tokens, Actual_tokens} of {[], []} -> same; {[], _} -> different; {_, []} -> different; {[{Expected_token, _} | Expected_rest], [{Actual_token, _} | Actual_rest]} -> case Expected_token =:= Actual_token of true -> compare_token_lists(Expected_rest, Actual_rest); false -> different end end. -file("src/squirrel.gleam", 425). ?DOC(false). -spec compare_code_snippets(binary(), binary()) -> check_result(). compare_code_snippets(Actual_code, Expected_code) -> Actual_tokens = begin _pipe = glexer:new(Actual_code), _pipe@1 = glexer:discard_comments(_pipe), _pipe@2 = glexer:discard_whitespace(_pipe@1), glexer:lex(_pipe@2) end, Expected_tokens = begin _pipe@3 = glexer:new(Expected_code), _pipe@4 = glexer:discard_comments(_pipe@3), _pipe@5 = glexer:discard_whitespace(_pipe@4), glexer:lex(_pipe@5) end, compare_token_lists(Expected_tokens, Actual_tokens). -file("src/squirrel.gleam", 463). -spec term_width() -> integer(). term_width() -> _pipe = term_size:columns(), _pipe@1 = gleam@result:unwrap(_pipe, 80), gleam@int:min(_pipe@1, 80). -file("src/squirrel.gleam", 576). -spec pluralise(integer(), binary(), binary()) -> binary(). pluralise(Count, Singular, Plural) -> case Count of 1 -> Singular; _ -> Plural end. -file("src/squirrel.gleam", 583). -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). -file("src/squirrel.gleam", 566). -spec text_with_header(binary(), binary()) -> glam@doc:document(). text_with_header(Header, Text) -> _pipe@1 = [glam@doc:from_string(Header), begin _pipe = flexible_string(Text), glam@doc:nest(_pipe, string:length(Header)) end], _pipe@2 = glam@doc:concat(_pipe@1), glam@doc:group(_pipe@2). -file("src/squirrel.gleam", 469). -spec report_written_queries( gleam@dict:dict(binary(), {integer(), list(squirrel@internal@error:error())}) ) -> {binary(), integer()}. report_written_queries(Dirs) -> {Ok, Errors@1} = begin gleam@dict:fold( Dirs, {0, []}, fun(Acc, _, _use2) -> {Oks, Errors} = _use2, {All_ok, All_errors} = Acc, {All_ok + Oks, begin _pipe = Errors, lists:append(_pipe, All_errors) end} end ) end, Errors_doc = begin _pipe@1 = gleam@list:map(Errors@1, fun squirrel@internal@error:to_doc/1), glam@doc:join(_pipe@1, glam@doc:lines(2)) end, Status_code = case Errors@1 of [_ | _] -> 1; [] -> 0 end, Report = case {Ok, Errors@1} of {0, [_ | _]} -> glam@doc:to_string(Errors_doc, term_width()); {0, []} -> _pipe@2 = [text_with_header( <<"🐿️ "/utf8>>, <<"I couldn't find any `*.sql` file to generate queries from."/utf8>> ), glam@doc:lines(2), flexible_string( <<"Hint: I look for all `*.sql` files in any directory called `sql` under your project's `src` directory."/utf8>> )], _pipe@3 = glam@doc:concat(_pipe@2), _pipe@4 = glam@doc:to_string(_pipe@3, term_width()), gleam_community@ansi:yellow(_pipe@4); {N, []} -> _pipe@5 = text_with_header( <<"🐿️ "/utf8>>, <<<<<<<<"Generated "/utf8, (erlang:integer_to_binary(N))/binary>>/binary, " "/utf8>>/binary, (pluralise(N, <<"query"/utf8>>, <<"queries"/utf8>>))/binary>>/binary, "!"/utf8>> ), _pipe@6 = glam@doc:to_string(_pipe@5, term_width()), gleam_community@ansi:green(_pipe@6); {N@1, [_ | _]} -> _pipe@7 = [Errors_doc, glam@doc:lines(2), text_with_header( <<"🥜 "/utf8>>, <<<<<<<<"I could still generate "/utf8, (erlang:integer_to_binary(N@1))/binary>>/binary, " "/utf8>>/binary, (pluralise( N@1, <<"query"/utf8>>, <<"queries"/utf8>> ))/binary>>/binary, "."/utf8>> )], _pipe@8 = glam@doc:concat(_pipe@7), glam@doc:to_string(_pipe@8, term_width()) end, {Report, Status_code}. -file("src/squirrel.gleam", 537). -spec report_checked_queries( gleam@dict:dict(binary(), {ok, nil} | {error, list(squirrel@internal@error:error())}) ) -> {binary(), integer()}. report_checked_queries(Dirs) -> Errors@1 = begin gleam@dict:fold(Dirs, [], fun(All_errors, _, Result) -> case Result of {error, Errors} -> _pipe = Errors, lists:append(_pipe, All_errors); {ok, _} -> All_errors end end) end, Status_code = case Errors@1 of [_ | _] -> 1; [] -> 0 end, Report = case Errors@1 of [_ | _] -> _pipe@1 = gleam@list:map( Errors@1, fun squirrel@internal@error:to_doc/1 ), _pipe@2 = glam@doc:join(_pipe@1, glam@doc:lines(2)), glam@doc:to_string(_pipe@2, term_width()); [] -> _pipe@3 = text_with_header(<<"🐿️ "/utf8>>, <<"All good!"/utf8>>), _pipe@4 = glam@doc:to_string(_pipe@3, term_width()), gleam_community@ansi:green(_pipe@4) end, {Report, Status_code}. -file("src/squirrel.gleam", 126). -spec help_text() -> glam@doc:document(). help_text() -> Nesting = 4, Title_line = glam@doc:from_string(<<"🐿️ Squirrel "/utf8, "v4.1.0"/utf8>>), Usage_line = glam@doc:concat( [glam@doc:zero_width_string( gleam_community@ansi:yellow(<<"Usage: "/utf8>>) ), glam@doc:zero_width_string( gleam_community@ansi:green(<<"gleam run -m squirrel"/utf8>>) ), glam@doc:from_string(<<" [COMMAND]"/utf8>>)] ), Check_command = begin _pipe@2 = [glam@doc:from_string(<<"check "/utf8>>), begin _pipe = <<"checks the generated code is up to date with the sql files"/utf8>>, _pipe@1 = flexible_string(_pipe), glam@doc:nest(_pipe@1, 7) end], _pipe@3 = glam@doc:concat(_pipe@2), glam@doc:group(_pipe@3) end, Commands = begin _pipe@4 = [glam@doc:zero_width_string( gleam_community@ansi:yellow(<<"Commands:"/utf8>>) ), glam@doc:nest(Check_command, Nesting)], glam@doc:join(_pipe@4, glam@doc:nest({line, 1}, Nesting)) end, _pipe@5 = [Title_line, Usage_line, Commands], glam@doc:join(_pipe@5, glam@doc:lines(2)). -file("src/squirrel.gleam", 354). -spec write_queries_to_file( list(squirrel@internal@query:typed_query()), binary() ) -> {ok, integer()} | {error, squirrel@internal@error:error()}. write_queries_to_file(Queries, File) -> gleam@bool:guard( Queries =:= [], {ok, 0}, fun() -> Directory = filepath:directory_name(File), _ = simplifile:create_directory_all(Directory), Code = squirrel@internal@query:generate_code( Queries, <<"v4.1.0"/utf8>> ), Try_write = begin _pipe = simplifile:write(File, Code), gleam@result:map_error( _pipe, fun(_capture) -> {cannot_write_to_file, File, _capture} end ) end, gleam@result:'try'( Try_write, fun(_) -> {ok, erlang:length(Queries)} end ) end ). -file("src/squirrel.gleam", 343). ?DOC( " Given the queries generated by `generate_queries`, tries to write those to\n" " their own file and returns a dictionary that - for each file - holds the\n" " number of queries that could be generated and a list of all the errors that\n" " took place.\n" ). -spec write_queries( gleam@dict:dict(binary(), {list(squirrel@internal@query:typed_query()), list(squirrel@internal@error:error())}) ) -> gleam@dict:dict(binary(), {integer(), list(squirrel@internal@error:error())}). write_queries(Queries) -> gleam@dict:map_values( Queries, fun(Directory, _use1) -> {Queries@1, Errors} = _use1, Output_file = directory_to_output_file(Directory), case write_queries_to_file(Queries@1, Output_file) of {ok, N} -> {N, Errors}; {error, Error} -> {erlang:length(Queries@1), [Error | Errors]} end end ). -file("src/squirrel.gleam", 416). ?DOC( " Checks that the given `file`'s code is the same that would be generated from\n" " the given `queries`.\n" " Returns `Same` if the code is ok, `Different` otherwise.\n" "\n" " > ⚠️ Note how this comparison doesn't take comments and whitespace into\n" " > account! If the only thing that changed are comments and/or formatting\n" " > two files will still be considered the same.\n" ). -spec check_queries_code(list(squirrel@internal@query:typed_query()), binary()) -> check_result(). check_queries_code(Queries, Actual_code) -> Expected_code = squirrel@internal@query:generate_code( Queries, <<"v4.1.0"/utf8>> ), compare_code_snippets(Actual_code, Expected_code). -file("src/squirrel.gleam", 378). -spec check_queries( gleam@dict:dict(binary(), {list(squirrel@internal@query:typed_query()), list(squirrel@internal@error:error())}) ) -> gleam@dict:dict(binary(), {ok, nil} | {error, list(squirrel@internal@error:error())}). check_queries(Queries) -> gleam@dict:map_values( Queries, fun(Directory, _use1) -> {Queries@1, Errors} = _use1, case Errors of [_ | _] -> {error, Errors}; [] -> Output_file = directory_to_output_file(Directory), case simplifile:read(Output_file) of {error, Reason} -> {error, [{cannot_read_file, Output_file, Reason}]}; {ok, Actual_code} -> case check_queries_code(Queries@1, Actual_code) of different -> {error, [{outdated_file, Output_file}]}; same -> {ok, nil} end end end end ). -file("src/squirrel.gleam", 191). ?DOC( " Creates a `ConnectionOptions` reading values from env variables and falling\n" " back to some defaults if any required one is not set.\n" ). -spec connection_options_from_variables() -> squirrel@internal@database@postgres:connection_options(). connection_options_from_variables() -> Host = begin _pipe = envoy_ffi:get(<<"PGHOST"/utf8>>), gleam@result:unwrap(_pipe, <<"localhost"/utf8>>) end, User = begin _pipe@1 = envoy_ffi:get(<<"PGUSER"/utf8>>), gleam@result:unwrap(_pipe@1, <<"postgres"/utf8>>) end, Password = begin _pipe@2 = envoy_ffi:get(<<"PGPASSWORD"/utf8>>), gleam@result:unwrap(_pipe@2, <<""/utf8>>) end, Database = begin _pipe@3 = envoy_ffi:get(<<"PGDATABASE"/utf8>>), _pipe@4 = gleam@result:'or'(_pipe@3, squirrel@internal@project:name()), gleam@result:unwrap(_pipe@4, <<"database"/utf8>>) end, Port = begin _pipe@5 = envoy_ffi:get(<<"PGPORT"/utf8>>), _pipe@6 = gleam@result:'try'(_pipe@5, fun gleam_stdlib:parse_int/1), gleam@result:unwrap(_pipe@6, 5432) end, Timeout_seconds = begin _pipe@7 = envoy_ffi:get(<<"PGCONNECT_TIMEOUT"/utf8>>), _pipe@8 = gleam@result:'try'(_pipe@7, fun gleam_stdlib:parse_int/1), gleam@result:unwrap(_pipe@8, 5) end, {connection_options, Host, Port, User, Password, Database, Timeout_seconds}. -file("src/squirrel.gleam", 221). ?DOC( " Parses a connection string into a `ConnectionOptions` failing if it has an\n" " invalid format instead of silently producing a default one.\n" ). -spec parse_connection_url(binary()) -> {ok, squirrel@internal@database@postgres:connection_options()} | {error, nil}. parse_connection_url(Raw) -> gleam@result:'try'( gleam_stdlib:uri_parse(Raw), fun(Uri) -> {uri, Scheme, Userinfo, Host, Port, Path, Query, _} = Uri, gleam@result:'try'(case Query of none -> {ok, []}; {some, Parameters} -> gleam_stdlib:parse_query(Parameters) end, fun(Parameters@1) -> gleam@result:'try'( check_scheme(Scheme), fun(_) -> {User, Password} = parse_user_and_password_from_userinfo( Userinfo ), Database = parse_database_from_path(Path), gleam@result:'try'( case gleam@list:key_find( Parameters@1, <<"connect_timeout"/utf8>> ) of {error, _} -> {ok, 5}; {ok, Timeout} -> gleam_stdlib:parse_int(Timeout) end, fun(Timeout@1) -> {ok, {connection_options, begin _pipe = Host, gleam@option:unwrap( _pipe, <<"localhost"/utf8>> ) end, begin _pipe@1 = Port, gleam@option:unwrap( _pipe@1, 5432 ) end, begin _pipe@2 = User, gleam@option:unwrap( _pipe@2, <<"postgres"/utf8>> ) end, begin _pipe@3 = Password, gleam@option:unwrap( _pipe@3, <<""/utf8>> ) end, begin _pipe@4 = Database, gleam@option:unwrap( _pipe@4, <<"database"/utf8>> ) end, Timeout@1}} end ) end ) end) end ). -file("src/squirrel.gleam", 167). ?DOC( " Returns the connection options to use to connect to the database.\n" " It first tries to read and parse a `DATABASE_URL` env variable (failing if\n" " it has an invalid format).\n" "\n" " If the `DATABASE_URL` variable is not set, it uses the Postgres env vars and\n" " some defaults if any of those are not set.\n" ). -spec connection_options() -> {ok, squirrel@internal@database@postgres:connection_options()} | {error, squirrel@internal@error:error()}. connection_options() -> case envoy_ffi:get(<<"DATABASE_URL"/utf8>>) of {ok, Url} -> _pipe = parse_connection_url(Url), gleam@result:replace_error(_pipe, {invalid_connection_string, Url}); {error, _} -> {ok, connection_options_from_variables()} end. -file("src/squirrel.gleam", 65). ?DOC( " 🐿️ Performs code generation for your Gleam project.\n" "\n" " `squirrel` is not configurable and will discover the queries to generate\n" " code for by relying on a conventional project's structure:\n" " - `squirrel` first looks for all directories called `sql` under the `src`\n" " directory of your Gleam project, and reads all the `*.sql` files in there\n" " (in glob terms `src/**/sql/*.sql`).\n" " - Each `*.sql` file _must contain a single query_ as it is turned into a\n" " Gleam function with the same name.\n" " - All functions coming from the same `sql` directory will be grouped under\n" " a Gleam file called `sql.gleam` at the same level: given a `src/$PATH/sql`\n" " directory, you'll end up with a generated `src/$PATH/sql.gleam` file.\n" "\n" " > ⚠️ In order to generate type safe code, `squirrel` has to connect\n" " > to your Postgres database. To know what host, user, etc. values to use\n" " > when connecting, it will read the `DATABASE_URL` env variable that has to\n" " > be a valid connection string with the following format:\n" " >\n" " > ```txt\n" " > postgres://user:password@host:port/database?connect_timeout=seconds\n" " > ```\n" " >\n" " > If a `DATABASE_URL` variable is not set, Squirrel will instead read your\n" " > [Postgres env variables](https://www.postgresql.org/docs/current/libpq-envars.html)\n" " > and use the following defaults if one is not set:\n" " > - `PGHOST`: `\"localhost\"`\n" " > - `PGPORT`: `5432`\n" " > - `PGUSER`: `\"postgres\"`\n" " > - `PGDATABASE`: the name of your Gleam project\n" " > - `PGPASSWORD`: `\"\"`\n" " > - `PGCONNECT_TIMEOUT`: `5` seconds\n" "\n" " > ⚠️ The generated code relies on the\n" " > [`pog`](https://hexdocs.pm/pog/) package to work, so make sure to add\n" " > that dependency to your project.\n" ). -spec main() -> nil. main() -> case {parse_cli_args(), connection_options()} of {{error, nil}, _} -> _pipe = help_text(), _pipe@1 = glam@doc:to_string(_pipe, term_width()), gleam_stdlib:println(_pipe@1), squirrel_ffi:exit(1); {_, {error, Error}} -> _pipe@2 = squirrel@internal@error:to_doc(Error), _pipe@3 = glam@doc:to_string(_pipe@2, term_width()), gleam_stdlib:println(_pipe@3), squirrel_ffi:exit(1); {{ok, Mode}, {ok, Options}} -> Generated_queries = begin _pipe@4 = walk(squirrel@internal@project:src()), _pipe@5 = maps:merge( _pipe@4, walk(squirrel@internal@project:test_()) ), _pipe@6 = maps:merge( _pipe@5, walk(squirrel@internal@project:dev()) ), generate_queries(_pipe@6, Options) end, {Report, Status_code} = case Mode of generate_code -> _pipe@7 = Generated_queries, _pipe@8 = write_queries(_pipe@7), report_written_queries(_pipe@8); check_generated_code -> _pipe@9 = Generated_queries, _pipe@10 = check_queries(_pipe@9), report_checked_queries(_pipe@10) end, gleam_stdlib:println(Report), squirrel_ffi:exit(Status_code) end.