Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .env
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
SERVICE_NAME=dmt
OTP_VERSION=28
OTP_VERSION=28.5
REBAR_VERSION=3.25
THRIFT_VERSION=0.14.2.3
CONFLUENT_PLATFORM_VERSION=5.1.2
CONFLUENT_PLATFORM_VERSION=5.1.2
# DATABASE_URL=postgresql://postgres:postgres@dmt_db:5432/dmt
ELP_VERSION=2026-02-27
ELP_OTP_VERSION=28
38 changes: 38 additions & 0 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Static analysis

on:
push:
branches:
- "master"
- "epic/**"
pull_request:
branches: ["**"]

permissions:
contents: read

jobs:
eqwalizer:
name: Eqwalizer
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Load .env
run: grep -v '^#' .env >> "$GITHUB_ENV"

- name: Build dev image
run: make dev-image

- name: Run eqwalizer
run: |
make wc-eqwalizer 2>&1 | tee /tmp/eqwalizer.log
# `elp eqwalize-all` exits 0 even when it reports errors; fail the
# job ourselves if any module produced errors.
if grep -qE "^[0-9]+ ERRORS" /tmp/eqwalizer.log; then
echo "::error::Eqwalizer reported errors"
exit 1
fi
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ bin

_build
_tmp

.claude
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
erlang 28.0
rebar 3.23.0
rebar 3.25.0
7 changes: 7 additions & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ ARG TARGETARCH
RUN wget -q -O- "https://github.com/valitydev/thrift/releases/download/${THRIFT_VERSION}/thrift-${THRIFT_VERSION}-linux-${TARGETARCH}.tar.gz" \
| tar -xvz -C /usr/local/bin/

# Install ELP (Erlang Language Platform) for eqWAlizer
ARG ELP_VERSION
ARG ELP_OTP_VERSION
RUN ELP_ARCH=$([ "${TARGETARCH}" = "arm64" ] && echo "aarch64" || echo "x86_64") && \
wget -q -O- "https://github.com/WhatsApp/erlang-language-platform/releases/download/${ELP_VERSION}/elp-linux-${ELP_ARCH}-unknown-linux-gnu-otp-${ELP_OTP_VERSION}.tar.gz" \
| tar -xvz -C /usr/local/bin/

RUN apt-get update && apt-get install -y cmake

# Set env
Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ check-format:
dialyze:
$(REBAR) as test dialyzer

eqwalizer:
$(REBAR) compile
# ERL_LIBS lets elp's erlang_service load compiled parse_transforms
# (e.g. dmt_domain_pt) when analysing modules that use them.
ERL_LIBS=$(CURDIR)/_build/default/lib elp eqwalize-all

release:
$(REBAR) as prod release

Expand Down
57 changes: 54 additions & 3 deletions apps/dmt/src/dmt_api_woody_utils.erl
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,51 @@

%% API

-type service_opts() :: #{
url := binary() | list(),
transport_opts => term(),
resolver_opts => term()
}.

%% Options passed to every thrift handler via woody's `handlers` config.
-type handler_options() :: #{default_handling_timeout := timeout(), atom() => term()}.

-export_type([service_opts/0, handler_options/0]).

-spec get_woody_client(atom()) -> woody_client:options().
get_woody_client(Service) ->
Services = genlib_app:env(dmt_api, services, #{}),
Services = get_services(genlib_app:env(dmt_api, services, #{})),
make_woody_client(maps:get(Service, Services)).

-spec make_woody_client(#{atom() => _}) -> woody_client:options().
-spec get_services(term()) -> #{atom() => service_opts()}.
get_services(Map) when is_map(Map) ->
maps:fold(fun fold_service/3, #{}, Map);
get_services(_) ->
#{}.

-spec fold_service(term(), term(), #{atom() => service_opts()}) ->
#{atom() => service_opts()}.
fold_service(K, #{url := Url} = V, Acc) when is_atom(K), is_binary(Url) ->
Acc#{K => build_service_opts(V, Url)};
fold_service(K, #{url := Url} = V, Acc) when is_atom(K), is_list(Url) ->
Acc#{K => build_service_opts(V, Url)};
fold_service(_, _, Acc) ->
Acc.

-spec build_service_opts(map(), binary() | list()) -> service_opts().
build_service_opts(V, Url) ->
Base = #{url => Url},
Base1 =
case maps:find(transport_opts, V) of
{ok, T} -> Base#{transport_opts => T};
error -> Base
end,
case maps:find(resolver_opts, V) of
{ok, R} -> Base1#{resolver_opts => R};
error -> Base1
end.

-spec make_woody_client(service_opts()) -> woody_client:options().
make_woody_client(#{url := Url} = Service) ->
lists:foldl(
fun(Opt, Acc) ->
Expand All @@ -31,7 +70,19 @@ make_woody_client(#{url := Url} = Service) ->

-spec get_woody_event_handlers() -> woody:ev_handlers().
get_woody_event_handlers() ->
genlib_app:env(dmt_api, woody_event_handlers, [scoper_woody_event_handler]).
Default = [scoper_woody_event_handler],
case genlib_app:env(dmt_api, woody_event_handlers, Default) of
Handler when is_atom(Handler) -> Handler;
{Mod, Opts} when is_atom(Mod) -> {Mod, Opts};
[_ | _] = List -> [ensure_ev_handler(H) || H <- List];
[] -> Default;
_ -> Default
end.

-spec ensure_ev_handler(term()) -> woody:ev_handler() | no_return().
ensure_ev_handler(Mod) when is_atom(Mod) -> Mod;
ensure_ev_handler({Mod, Opts}) when is_atom(Mod) -> {Mod, Opts};
ensure_ev_handler(Other) -> erlang:error({bad_ev_handler, Other}).

-spec ensure_woody_deadline_set(woody_context:ctx(), woody_deadline:deadline()) -> woody_context:ctx().
ensure_woody_deadline_set(WoodyContext, Default) ->
Expand Down
8 changes: 7 additions & 1 deletion apps/dmt/src/dmt_app.erl
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@

-export([start/2, stop/1]).

-spec start(application:start_type(), term()) ->
{ok, pid()} | {ok, pid(), term()} | {error, term()}.
start(_StartType, _StartArgs) ->
dmt_sup:start_link().
case dmt_sup:start_link() of
ignore -> {error, ignore};
Other -> Other
end.

-spec stop(term()) -> ok.
stop(_State) ->
ok.

Expand Down
14 changes: 14 additions & 0 deletions apps/dmt/src/dmt_author.erl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

%% Public API

-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl").

-define(POOL_NAME, author_pool).

-export([
Expand All @@ -11,21 +13,33 @@
delete/1
]).

-type author_id() :: binary().
-type name() :: binary().
-type email() :: binary().
-type author() :: dmsl_domain_conf_v2_thrift:'Author'().

-export_type([author_id/0, name/0, email/0, author/0]).

%% Optional: Extended API (can be uncommented if these functions should be exposed)
%% -export([
%% list/2,
%% search/2
%% ]).

-spec insert(name(), email()) ->
{ok, author_id()} | {ok, {already_exists, author_id()}} | {error, unknown}.
insert(Name, Email) ->
dmt_author_database:insert(?POOL_NAME, Name, Email).

-spec get(author_id()) -> {ok, author()} | {error, author_not_found | term()}.
get(AuthorID) ->
dmt_author_database:get(?POOL_NAME, AuthorID).

-spec get_by_email(email()) -> {ok, author()} | {error, author_not_found | term()}.
get_by_email(Email) ->
dmt_author_database:get_by_email(?POOL_NAME, Email).

-spec delete(author_id()) -> ok | {error, author_not_found | term()}.
delete(AuthorID) ->
dmt_author_database:delete(?POOL_NAME, AuthorID).

Expand Down
35 changes: 30 additions & 5 deletions apps/dmt/src/dmt_author_database.erl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@
search/3
]).

-type worker() :: dmt_database:worker().
-type author_id() :: dmt_author:author_id().
-type name() :: dmt_author:name().
-type email() :: dmt_author:email().
-type author() :: dmt_author:author().

-export_type([worker/0, author_id/0, name/0, email/0, author/0]).

-spec insert(worker(), name(), email()) ->
{ok, author_id()} | {ok, {already_exists, author_id()}} | {error, unknown}.
insert(Worker, Name, Email) ->
Sql = """
INSERT INTO author (name, email)
Expand All @@ -32,6 +42,7 @@ insert(Worker, Name, Email) ->
{error, unknown}
end.

-spec get(worker(), author_id()) -> {ok, author()} | {error, author_not_found | term()}.
get(Worker, AuthorID) ->
case is_uuid(AuthorID) of
true ->
Expand All @@ -40,6 +51,7 @@ get(Worker, AuthorID) ->
{error, author_not_found}
end.

-spec get_(worker(), author_id()) -> {ok, author()} | {error, author_not_found | term()}.
get_(Worker, AuthorID) ->
Sql = """
SELECT id, name, email
Expand All @@ -60,6 +72,7 @@ get_(Worker, AuthorID) ->
{error, Reason}
end.

-spec get_by_email(worker(), email()) -> {ok, author()} | {error, author_not_found | term()}.
get_by_email(Worker, Email) ->
Sql = """
SELECT id, name, email
Expand All @@ -80,6 +93,7 @@ get_by_email(Worker, Email) ->
{error, Reason}
end.

-spec delete(worker(), author_id()) -> ok | {error, author_not_found | term()}.
delete(Worker, AuthorID) ->
case is_uuid(AuthorID) of
true ->
Expand All @@ -88,6 +102,7 @@ delete(Worker, AuthorID) ->
{error, author_not_found}
end.

-spec delete_(worker(), author_id()) -> ok | {error, author_not_found | term()}.
delete_(Worker, AuthorID) ->
Sql = """
DELETE FROM author
Expand All @@ -103,6 +118,8 @@ delete_(Worker, AuthorID) ->
{error, Reason}
end.

-spec list(worker(), pos_integer(), non_neg_integer()) ->
{ok, [author()]} | {error, term()}.
list(Worker, Limit, Offset) ->
Sql = """
SELECT id, name, email
Expand All @@ -126,6 +143,7 @@ list(Worker, Limit, Offset) ->
{error, Reason}
end.

-spec search(worker(), binary(), pos_integer()) -> {ok, [author()]} | {error, term()}.
search(Worker, SearchTerm, Limit) ->
Sql = """
SELECT id, name, email FROM author
Expand All @@ -152,11 +170,18 @@ search(Worker, SearchTerm, Limit) ->

%% Internal functions

is_uuid(UUID) ->
-spec is_uuid(term()) -> boolean().
is_uuid(<<UUID:32/binary>>) ->
try_string_to_uuid(UUID);
is_uuid(<<UUID:36/binary>>) ->
try_string_to_uuid(UUID);
is_uuid(_) ->
false.

-spec try_string_to_uuid(uuid:uuid_string()) -> boolean().
try_string_to_uuid(UUID) ->
try uuid:string_to_uuid(UUID) of
_UUID ->
true
_ -> true
catch
exit:badarg ->
false
exit:badarg -> false
end.
17 changes: 16 additions & 1 deletion apps/dmt/src/dmt_author_handler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,32 @@

-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl").

-behaviour(woody_server_thrift_handler).

%% API
-export([handle_function/4]).

-type options() :: dmt_api_woody_utils:handler_options().

-export_type([options/0]).

-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), options()) ->
{ok, woody:result()} | no_return().
handle_function(Function, Args, WoodyContext0, Options) ->
DefaultDeadline = woody_deadline:from_timeout(default_handling_timeout(Options)),
WoodyContext = dmt_api_woody_utils:ensure_woody_deadline_set(WoodyContext0, DefaultDeadline),
do_handle_function(Function, Args, WoodyContext, Options).
%% Cast: `woody:args()` is `tuple() | any()`; each `do_handle_function/4`
%% clause pattern-matches a specific arg tuple guaranteed by the thrift
%% schema. The shape is enforced by woody at deserialisation, not by
%% the type system, so we cross the trust boundary explicitly here.
do_handle_function(Function, eqwalizer:dynamic_cast(Args), WoodyContext, Options).

-spec default_handling_timeout(options()) -> timeout().
default_handling_timeout(#{default_handling_timeout := Timeout}) ->
Timeout.

-spec do_handle_function(woody:func(), eqwalizer:dynamic(tuple()), woody_context:ctx(), options()) ->
{ok, woody:result()} | no_return().
%% Implement the Create function
do_handle_function('Create', {Params}, _Context, _Options) ->
#domain_conf_v2_AuthorParams{email = Email, name = Name} = Params,
Expand Down
Loading
Loading