mirror of
https://github.com/valitydev/url-shortener.git
synced 2024-11-06 01:55:19 +00:00
Add formatter (#33)
* Add erlfmt * Update build-utils * Apply erlfmt * Exclude generated swag client and server from format check
This commit is contained in:
parent
8e89bdf915
commit
f0cd85e458
9
Makefile
9
Makefile
@ -19,7 +19,8 @@ BASE_IMAGE_TAG := da0ab769f01b650b389d18fc85e7418e727cbe96
|
||||
# Build image tag to be used
|
||||
BUILD_IMAGE_TAG := 442c2c274c1d8e484e5213089906a4271641d95e
|
||||
|
||||
CALL_ANYWHERE := all submodules rebar-update compile xref lint dialyze start devrel release clean distclean
|
||||
CALL_ANYWHERE := all submodules rebar-update compile xref lint dialyze start \
|
||||
devrel release clean distclean check_format format
|
||||
|
||||
CALL_W_CONTAINER := $(CALL_ANYWHERE) test
|
||||
|
||||
@ -49,6 +50,12 @@ xref: submodules
|
||||
lint:
|
||||
elvis rock
|
||||
|
||||
check_format:
|
||||
$(REBAR) fmt -c
|
||||
|
||||
format:
|
||||
$(REBAR) fmt -w
|
||||
|
||||
dialyze:
|
||||
$(REBAR) dialyzer
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
%% Application callbacks
|
||||
-export([start/2]).
|
||||
-export([stop /1]).
|
||||
-export([stop/1]).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
@ -17,26 +17,22 @@
|
||||
%%
|
||||
|
||||
-spec start(normal, any()) -> {ok, pid()} | {error, any()}.
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
?MODULE:start_link().
|
||||
|
||||
-spec stop(any()) -> ok.
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
||||
|
||||
%% API
|
||||
|
||||
-spec start_link() -> {ok, pid()} | {error, {already_started, pid()}}.
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
%% Supervisor callbacks
|
||||
|
||||
-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
|
||||
|
||||
init([]) ->
|
||||
HealthRoutes = get_health_routes(genlib_app:env(?MODULE, health_check, #{})),
|
||||
{ok, {
|
||||
@ -51,7 +47,7 @@ get_health_routes(Check) ->
|
||||
|
||||
enable_health_logging(Check = #{}) ->
|
||||
maps:map(
|
||||
fun (_, V = {_, _, _}) ->
|
||||
fun(_, V = {_, _, _}) ->
|
||||
#{runner => V, event_handler => {erl_health_event_handler, []}}
|
||||
end,
|
||||
Check
|
||||
@ -59,7 +55,8 @@ enable_health_logging(Check = #{}) ->
|
||||
|
||||
get_processor_childspecs(Opts, HealthRoutes) ->
|
||||
{ok, IP} = inet:parse_address(maps:get(ip, Opts, "::")),
|
||||
[woody_server:child_spec(
|
||||
[
|
||||
woody_server:child_spec(
|
||||
?MODULE,
|
||||
#{
|
||||
ip => IP,
|
||||
@ -75,7 +72,8 @@ get_processor_childspecs(Opts, HealthRoutes) ->
|
||||
],
|
||||
additional_routes => HealthRoutes
|
||||
}
|
||||
)].
|
||||
)
|
||||
].
|
||||
|
||||
get_api_childspecs(Opts, HealthRoutes) ->
|
||||
AuthorizerSpec = shortener_authorizer_jwt:get_child_spec(maps:get(authorizer, Opts)),
|
||||
|
@ -27,27 +27,19 @@
|
||||
|
||||
%%
|
||||
|
||||
-spec new() ->
|
||||
t().
|
||||
|
||||
-spec new() -> t().
|
||||
new() ->
|
||||
[].
|
||||
|
||||
-spec to_list(t()) ->
|
||||
[{scope(), permission()}].
|
||||
|
||||
-spec to_list(t()) -> [{scope(), permission()}].
|
||||
to_list(ACL) ->
|
||||
[{S, P} || {{_, S}, P} <- ACL].
|
||||
|
||||
-spec from_list([{scope(), permission()}]) ->
|
||||
t().
|
||||
|
||||
-spec from_list([{scope(), permission()}]) -> t().
|
||||
from_list(L) ->
|
||||
lists:foldl(fun ({S, P}, ACL) -> insert_scope(S, P, ACL) end, new(), L).
|
||||
|
||||
-spec insert_scope(scope(), permission(), t()) ->
|
||||
t().
|
||||
lists:foldl(fun({S, P}, ACL) -> insert_scope(S, P, ACL) end, new(), L).
|
||||
|
||||
-spec insert_scope(scope(), permission(), t()) -> t().
|
||||
insert_scope(Scope, Permission, ACL) ->
|
||||
Priority = compute_priority(Scope, Permission),
|
||||
insert({{Priority, Scope}, [Permission]}, ACL).
|
||||
@ -62,9 +54,7 @@ insert({PS, _} = V, [{PS0, _} | _] = Vs) when PS > PS0 ->
|
||||
insert(V, []) ->
|
||||
[V].
|
||||
|
||||
-spec remove_scope(scope(), permission(), t()) ->
|
||||
t().
|
||||
|
||||
-spec remove_scope(scope(), permission(), t()) -> t().
|
||||
remove_scope(Scope, Permission, ACL) ->
|
||||
Priority = compute_priority(Scope, Permission),
|
||||
remove({{Priority, Scope}, [Permission]}, ACL).
|
||||
@ -107,9 +97,7 @@ compute_permission_priority(V) ->
|
||||
|
||||
%%
|
||||
|
||||
-spec match(scope(), t()) ->
|
||||
[permission()].
|
||||
|
||||
-spec match(scope(), t()) -> [permission()].
|
||||
match(Scope, ACL) when length(Scope) > 0 ->
|
||||
match_rules(Scope, ACL);
|
||||
match(Scope, _) ->
|
||||
@ -144,9 +132,7 @@ match_scope(_, _) ->
|
||||
|
||||
%%
|
||||
|
||||
-spec decode([binary()]) ->
|
||||
t().
|
||||
|
||||
-spec decode([binary()]) -> t().
|
||||
decode(V) ->
|
||||
lists:foldl(fun decode_entry/2, new(), V).
|
||||
|
||||
@ -180,7 +166,9 @@ decode_scope_frag_resource(V, ID, H) ->
|
||||
{{R, ID}, delve(R, H)}.
|
||||
|
||||
decode_resource(V) ->
|
||||
try binary_to_existing_atom(V, utf8) catch
|
||||
try
|
||||
binary_to_existing_atom(V, utf8)
|
||||
catch
|
||||
error:badarg ->
|
||||
error({badarg, {resource, V}})
|
||||
end.
|
||||
@ -194,16 +182,19 @@ decode_permission(V) ->
|
||||
|
||||
%%
|
||||
|
||||
-spec encode(t()) ->
|
||||
[binary()].
|
||||
|
||||
-spec encode(t()) -> [binary()].
|
||||
encode(ACL) ->
|
||||
lists:flatmap(fun encode_entry/1, ACL).
|
||||
|
||||
encode_entry({{_Priority, Scope}, Permissions}) ->
|
||||
S = encode_scope(Scope),
|
||||
[begin P = encode_permission(Permission), <<S/binary, ":", P/binary>> end
|
||||
|| Permission <- Permissions].
|
||||
[
|
||||
begin
|
||||
P = encode_permission(Permission),
|
||||
<<S/binary, ":", P/binary>>
|
||||
end
|
||||
|| Permission <- Permissions
|
||||
].
|
||||
|
||||
encode_scope(Scope) ->
|
||||
Hierarchy = get_resource_hierarchy(),
|
||||
|
@ -9,9 +9,7 @@
|
||||
-export_type([context/0]).
|
||||
-export_type([claims/0]).
|
||||
|
||||
-spec authorize_api_key(swag_server:operation_id(), swag_server:api_key()) ->
|
||||
{true, Context :: context()} | false.
|
||||
|
||||
-spec authorize_api_key(swag_server:operation_id(), swag_server:api_key()) -> {true, Context :: context()} | false.
|
||||
authorize_api_key(OperationID, ApiKey) ->
|
||||
case parse_api_key(ApiKey) of
|
||||
{ok, {Type, Credentials}} ->
|
||||
@ -30,9 +28,7 @@ authorize_api_key(OperationID, ApiKey) ->
|
||||
log_auth_error(OperationID, Error) ->
|
||||
logger:info("API Key authorization failed for ~p due to ~p", [OperationID, Error]).
|
||||
|
||||
-spec parse_api_key(swag_server:api_key()) ->
|
||||
{ok, {bearer, Credentials :: binary()}} | {error, Reason :: atom()}.
|
||||
|
||||
-spec parse_api_key(swag_server:api_key()) -> {ok, {bearer, Credentials :: binary()}} | {error, Reason :: atom()}.
|
||||
parse_api_key(ApiKey) ->
|
||||
case ApiKey of
|
||||
<<"Bearer ", Credentials/binary>> ->
|
||||
@ -43,7 +39,6 @@ parse_api_key(ApiKey) ->
|
||||
|
||||
-spec authorize_api_key(swag_server:operation_id(), Type :: atom(), Credentials :: binary()) ->
|
||||
{ok, context()} | {error, Reason :: atom()}.
|
||||
|
||||
authorize_api_key(_OperationID, bearer, Token) ->
|
||||
shortener_authorizer_jwt:verify(Token).
|
||||
|
||||
@ -51,7 +46,6 @@ authorize_api_key(_OperationID, bearer, Token) ->
|
||||
OperationID :: swag_server:operation_id(),
|
||||
Slug :: shortener_slug:slug() | no_slug,
|
||||
Context :: context().
|
||||
|
||||
authorize_operation(OperationID, Slug, {{SubjectID, ACL}, _Claims}) ->
|
||||
Owner = get_slug_owner(Slug),
|
||||
Permissions = shortener_acl:match(['shortened-urls'], ACL),
|
||||
@ -63,7 +57,6 @@ authorize_operation(OperationID, Slug, {{SubjectID, ACL}, _Claims}) ->
|
||||
end.
|
||||
|
||||
-spec get_slug_owner(shortener_slug:slug() | no_slug) -> shortener_slug:owner() | undefined.
|
||||
|
||||
get_slug_owner(no_slug) ->
|
||||
undefined;
|
||||
get_slug_owner(#{owner := Owner}) ->
|
||||
|
@ -28,9 +28,9 @@
|
||||
-type subject_id() :: binary().
|
||||
-type t() :: {subject(), claims()}.
|
||||
-type expiration() ::
|
||||
{lifetime, Seconds :: pos_integer()} |
|
||||
{deadline, UnixTs :: pos_integer()} |
|
||||
unlimited.
|
||||
{lifetime, Seconds :: pos_integer()}
|
||||
| {deadline, UnixTs :: pos_integer()}
|
||||
| unlimited.
|
||||
|
||||
-export_type([t/0]).
|
||||
-export_type([subject/0]).
|
||||
@ -56,9 +56,7 @@
|
||||
-type keysource() ::
|
||||
{pem_file, file:filename()}.
|
||||
|
||||
-spec get_child_spec(options()) ->
|
||||
supervisor:child_spec() | no_return().
|
||||
|
||||
-spec get_child_spec(options()) -> supervisor:child_spec() | no_return().
|
||||
get_child_spec(Options) ->
|
||||
#{
|
||||
id => ?MODULE,
|
||||
@ -70,7 +68,7 @@ parse_options(Options) ->
|
||||
Keyset = maps:get(keyset, Options, #{}),
|
||||
_ = is_map(Keyset) orelse exit({invalid_option, keyset, Keyset}),
|
||||
_ = genlib_map:foreach(
|
||||
fun (K, V) ->
|
||||
fun(K, V) ->
|
||||
is_keysource(V) orelse exit({invalid_option, K, V})
|
||||
end,
|
||||
Keyset
|
||||
@ -85,9 +83,7 @@ is_keysource(_) ->
|
||||
|
||||
%%
|
||||
|
||||
-spec init({keyset(), {ok, keyname()} | error}) ->
|
||||
{ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
|
||||
|
||||
-spec init({keyset(), {ok, keyname()} | error}) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
|
||||
init({Keyset, Signee}) ->
|
||||
ok = create_table(),
|
||||
KeyInfos = maps:map(fun ensure_store_key/2, Keyset),
|
||||
@ -125,9 +121,7 @@ select_signee(error, _KeyInfos) ->
|
||||
verify => boolean()
|
||||
}.
|
||||
|
||||
-spec store_key(keyname(), {pem_file, file:filename()}) ->
|
||||
{ok, keyinfo()} | {error, file:posix() | {unknown_key, _}}.
|
||||
|
||||
-spec store_key(keyname(), {pem_file, file:filename()}) -> {ok, keyinfo()} | {error, file:posix() | {unknown_key, _}}.
|
||||
store_key(Keyname, {pem_file, Filename}) ->
|
||||
store_key(Keyname, {pem_file, Filename}, #{
|
||||
kid => fun derive_kid_from_public_key_pem_entry/1
|
||||
@ -140,12 +134,11 @@ derive_kid_from_public_key_pem_entry(JWK) ->
|
||||
base64url:encode(crypto:hash(sha256, Data)).
|
||||
|
||||
-type store_opts() :: #{
|
||||
kid => fun ((key()) -> kid())
|
||||
kid => fun((key()) -> kid())
|
||||
}.
|
||||
|
||||
-spec store_key(keyname(), {pem_file, file:filename()}, store_opts()) ->
|
||||
{ok, keyinfo()} | {error, file:posix() | {unknown_key, _}}.
|
||||
|
||||
store_key(Keyname, {pem_file, Filename}, Opts) ->
|
||||
case jose_jwk:from_pem_file(Filename) of
|
||||
JWK = #jose_jwk{} ->
|
||||
@ -170,18 +163,25 @@ construct_key(KID, JWK) ->
|
||||
#{
|
||||
jwk => JWK,
|
||||
kid => KID,
|
||||
signer => try jose_jwk:signer(JWK) catch error:_ -> undefined end,
|
||||
verifier => try jose_jwk:verifier(JWK) catch error:_ -> undefined end
|
||||
signer =>
|
||||
try
|
||||
jose_jwk:signer(JWK)
|
||||
catch
|
||||
error:_ -> undefined
|
||||
end,
|
||||
verifier =>
|
||||
try
|
||||
jose_jwk:verifier(JWK)
|
||||
catch
|
||||
error:_ -> undefined
|
||||
end
|
||||
}.
|
||||
|
||||
%%
|
||||
|
||||
-spec issue(t(), expiration()) ->
|
||||
{ok, token()} |
|
||||
{error,
|
||||
nonexistent_signee
|
||||
}.
|
||||
|
||||
{ok, token()}
|
||||
| {error, nonexistent_signee}.
|
||||
issue(Auth, Expiration) ->
|
||||
case get_signee_key() of
|
||||
Key = #{} ->
|
||||
@ -220,20 +220,17 @@ sign(#{kid := KID, jwk := JWK, signer := #{} = JWS}, Claims) ->
|
||||
%%
|
||||
|
||||
-spec verify(token()) ->
|
||||
{ok, t()} |
|
||||
{error,
|
||||
{ok, t()}
|
||||
| {error,
|
||||
{invalid_token,
|
||||
badarg |
|
||||
{badarg, term()} |
|
||||
{missing, atom()} |
|
||||
expired |
|
||||
{malformed_acl, term()}
|
||||
} |
|
||||
{nonexistent_key, kid()} |
|
||||
invalid_operation |
|
||||
invalid_signature
|
||||
}.
|
||||
|
||||
badarg
|
||||
| {badarg, term()}
|
||||
| {missing, atom()}
|
||||
| expired
|
||||
| {malformed_acl, term()}}
|
||||
| {nonexistent_key, kid()}
|
||||
| invalid_operation
|
||||
| invalid_signature}.
|
||||
verify(Token) ->
|
||||
try
|
||||
{_, ExpandedToken} = jose_jws:expand(Token),
|
||||
@ -303,9 +300,9 @@ get_alg(#{}) ->
|
||||
|
||||
get_validators() ->
|
||||
[
|
||||
{token_id , <<"jti">> , fun check_presence/2},
|
||||
{subject_id , <<"sub">> , fun check_presence/2},
|
||||
{expires_at , <<"exp">> , fun check_expiration/2}
|
||||
{token_id, <<"jti">>, fun check_presence/2},
|
||||
{subject_id, <<"sub">>, fun check_presence/2},
|
||||
{expires_at, <<"exp">>, fun check_expiration/2}
|
||||
].
|
||||
|
||||
check_presence(_, V) when is_binary(V) ->
|
||||
@ -342,13 +339,15 @@ encode_roles(Roles) ->
|
||||
}
|
||||
}.
|
||||
|
||||
decode_roles(Claims = #{
|
||||
decode_roles(
|
||||
Claims = #{
|
||||
<<"resource_access">> := #{
|
||||
<<"url-shortener">> := #{
|
||||
<<"roles">> := Roles
|
||||
}
|
||||
}
|
||||
}) when is_list(Roles) ->
|
||||
}
|
||||
) when is_list(Roles) ->
|
||||
{Roles, maps:remove(<<"resource_access">>, Claims)};
|
||||
decode_roles(_) ->
|
||||
throw({invalid_token, {missing, acl}}).
|
||||
@ -379,6 +378,7 @@ get_signee_key() ->
|
||||
undefined ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
base64url_to_map(V) when is_binary(V) ->
|
||||
|
@ -1,4 +1,5 @@
|
||||
-module(shortener_cors_policy).
|
||||
|
||||
-behaviour(cowboy_cors_policy).
|
||||
|
||||
-export([policy_init/1]).
|
||||
@ -7,17 +8,14 @@
|
||||
-export([allowed_methods/2]).
|
||||
|
||||
-spec policy_init(cowboy_req:req()) -> {ok, cowboy_req:req(), any()}.
|
||||
|
||||
policy_init(Req) ->
|
||||
{ok, Req, undefined}.
|
||||
|
||||
-spec allowed_origins(cowboy_req:req(), any()) -> {'*', any()}.
|
||||
|
||||
allowed_origins(_, State) ->
|
||||
{'*', State}.
|
||||
|
||||
-spec allowed_headers(cowboy_req:req(), any()) -> {[binary()], any()}.
|
||||
|
||||
allowed_headers(_, State) ->
|
||||
{[
|
||||
<<"accept">>,
|
||||
@ -26,9 +24,9 @@ allowed_headers(_, State) ->
|
||||
<<"content-type">>,
|
||||
<<"x-request-id">>,
|
||||
<<"x-requested-with">>
|
||||
], State}.
|
||||
],
|
||||
State}.
|
||||
|
||||
-spec allowed_methods(cowboy_req:req(), any()) -> {[binary()], any()}.
|
||||
|
||||
allowed_methods(_, State) ->
|
||||
{[<<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], State}.
|
||||
|
@ -3,12 +3,14 @@
|
||||
%% Swagger handler
|
||||
|
||||
-behaviour(swag_server_logic_handler).
|
||||
|
||||
-export([authorize_api_key/3]).
|
||||
-export([handle_request/4]).
|
||||
|
||||
%% Cowboy http handler
|
||||
|
||||
-behaviour(cowboy_handler).
|
||||
|
||||
-export([init/2]).
|
||||
-export([terminate/3]).
|
||||
|
||||
@ -24,14 +26,11 @@
|
||||
|
||||
-spec authorize_api_key(operation_id(), swag_server:api_key(), swag_server:handler_opts(_)) ->
|
||||
Result :: false | {true, shortener_auth:context()}.
|
||||
|
||||
authorize_api_key(OperationID, ApiKey, _Opts) ->
|
||||
ok = scoper:add_scope('swag.server', #{operation => OperationID}),
|
||||
shortener_auth:authorize_api_key(OperationID, ApiKey).
|
||||
|
||||
-spec handle_request(operation_id(), request_data(), request_ctx(), any()) ->
|
||||
{ok | error, swag_server:response()}.
|
||||
|
||||
-spec handle_request(operation_id(), request_data(), request_ctx(), any()) -> {ok | error, swag_server:response()}.
|
||||
handle_request(OperationID, Req, Context, _Opts) ->
|
||||
try
|
||||
AuthContext = get_auth_context(Context),
|
||||
@ -52,7 +51,6 @@ handle_request(OperationID, Req, Context, _Opts) ->
|
||||
end.
|
||||
|
||||
-spec prefetch_slug(request_data(), woody_context:ctx()) -> shortener_slug:slug() | no_slug.
|
||||
|
||||
prefetch_slug(#{'shortenedUrlID' := ID}, WoodyCtx) ->
|
||||
case shortener_slug:get(ID, WoodyCtx) of
|
||||
{ok, Slug} ->
|
||||
@ -96,13 +94,14 @@ handle_woody_error(_Source, result_unknown, _Details) ->
|
||||
|
||||
-spec process_request(operation_id(), request_data(), shortener_slug:slug(), subject_id(), woody_context:ctx()) ->
|
||||
{ok | error, swag_server:response()}.
|
||||
|
||||
process_request(
|
||||
'ShortenUrl',
|
||||
#{'ShortenedUrlParams' := #{
|
||||
#{
|
||||
'ShortenedUrlParams' := #{
|
||||
<<"sourceUrl">> := SourceUrl,
|
||||
<<"expiresAt">> := ExpiresAt
|
||||
}},
|
||||
}
|
||||
},
|
||||
no_slug,
|
||||
SubjectID,
|
||||
WoodyCtx
|
||||
@ -112,12 +111,12 @@ process_request(
|
||||
Slug = shortener_slug:create(SourceUrl, parse_timestamp(ExpiresAt), SubjectID, WoodyCtx),
|
||||
{ok, {201, #{}, construct_shortened_url(Slug)}};
|
||||
false ->
|
||||
{ok, {400, #{}, #{
|
||||
{ok,
|
||||
{400, #{}, #{
|
||||
<<"code">> => <<"forbidden_source_url">>,
|
||||
<<"message">> => <<"Source URL is forbidden">>
|
||||
}}}
|
||||
end;
|
||||
|
||||
process_request(
|
||||
'GetShortenedUrl',
|
||||
_Req,
|
||||
@ -134,7 +133,6 @@ process_request(
|
||||
_WoodyCtx
|
||||
) ->
|
||||
{ok, {200, #{}, construct_shortened_url(Slug)}};
|
||||
|
||||
process_request(
|
||||
'DeleteShortenedUrl',
|
||||
#{'shortenedUrlID' := ID},
|
||||
@ -151,7 +149,7 @@ process_request(
|
||||
|
||||
validate_source_url(SourceUrl) ->
|
||||
lists:any(
|
||||
fun (Pattern) -> is_source_url_valid(SourceUrl, Pattern) end,
|
||||
fun(Pattern) -> is_source_url_valid(SourceUrl, Pattern) end,
|
||||
get_source_url_whitelist()
|
||||
).
|
||||
|
||||
@ -203,12 +201,11 @@ get_source_url_whitelist() ->
|
||||
-type request() :: cowboy_req:req().
|
||||
-type terminate_reason() :: {normal, shutdown} | {error, atom()}.
|
||||
|
||||
-spec init(request(), _) ->
|
||||
{ok, request(), state()}.
|
||||
|
||||
-spec init(request(), _) -> {ok, request(), state()}.
|
||||
init(Req, Opts) ->
|
||||
ID = cowboy_req:binding('shortenedUrlID', Req),
|
||||
Req1 = case shortener_slug:get(ID, woody_context:new()) of
|
||||
Req1 =
|
||||
case shortener_slug:get(ID, woody_context:new()) of
|
||||
{ok, #{source := Source, expires_at := ExpiresAt}} ->
|
||||
Seconds = genlib_rfc3339:parse(ExpiresAt, second),
|
||||
{Date, Time} = calendar:system_time_to_universal_time(Seconds, second),
|
||||
@ -223,8 +220,6 @@ init(Req, Opts) ->
|
||||
end,
|
||||
{ok, Req1, Opts}.
|
||||
|
||||
-spec terminate(terminate_reason(), request(), state()) ->
|
||||
ok.
|
||||
|
||||
-spec terminate(terminate_reason(), request(), state()) -> ok.
|
||||
terminate(_Reason, _Req, _St) ->
|
||||
ok.
|
||||
|
@ -1,4 +1,5 @@
|
||||
-module(shortener_slug).
|
||||
|
||||
-include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl").
|
||||
|
||||
%% API
|
||||
@ -18,7 +19,8 @@
|
||||
|
||||
%%
|
||||
|
||||
-type timestamp() :: binary(). % RFC 3339
|
||||
% RFC 3339
|
||||
-type timestamp() :: binary().
|
||||
|
||||
-type id() :: binary().
|
||||
-type source() :: binary().
|
||||
@ -31,15 +33,13 @@
|
||||
owner => owner() | undefined,
|
||||
expires_at => expiration()
|
||||
}.
|
||||
|
||||
-export_type([slug/0]).
|
||||
-export_type([owner/0]).
|
||||
|
||||
-type ctx() :: woody_context:ctx().
|
||||
|
||||
|
||||
-spec create(source(), expiration(), owner(), ctx()) ->
|
||||
slug().
|
||||
|
||||
-spec create(source(), expiration(), owner(), ctx()) -> slug().
|
||||
create(Source, ExpiresAt, Owner, Ctx) ->
|
||||
create(Source, ExpiresAt, Owner, 0, Ctx).
|
||||
|
||||
@ -53,9 +53,7 @@ create(Source, ExpiresAt, Owner, Attempt, Ctx) ->
|
||||
create(Source, ExpiresAt, Owner, Attempt + 1, Ctx)
|
||||
end.
|
||||
|
||||
-spec get(id(), ctx()) ->
|
||||
{ok, slug()} | {error, notfound}.
|
||||
|
||||
-spec get(id(), ctx()) -> {ok, slug()} | {error, notfound}.
|
||||
get(ID, Ctx) ->
|
||||
case get_machine_history(ID, Ctx) of
|
||||
{ok, History} ->
|
||||
@ -65,9 +63,7 @@ get(ID, Ctx) ->
|
||||
{error, notfound}
|
||||
end.
|
||||
|
||||
-spec remove(id(), ctx()) ->
|
||||
ok | {error, notfound}.
|
||||
|
||||
-spec remove(id(), ctx()) -> ok | {error, notfound}.
|
||||
remove(ID, Ctx) ->
|
||||
case remove_machine(ID, Ctx) of
|
||||
{ok, _} ->
|
||||
@ -88,10 +84,12 @@ format_id(ID) ->
|
||||
genlib_format:format_int_base(ID, 62).
|
||||
|
||||
get_hash_algorithm() ->
|
||||
{ok, V} = application:get_env(shortener, hash_algorithm), V.
|
||||
{ok, V} = application:get_env(shortener, hash_algorithm),
|
||||
V.
|
||||
|
||||
get_space_size() ->
|
||||
{ok, V} = application:get_env(shortener, space_size), V.
|
||||
{ok, V} = application:get_env(shortener, space_size),
|
||||
V.
|
||||
|
||||
%%
|
||||
|
||||
@ -139,6 +137,7 @@ call_service(Service, Method, Args, ClientOpts, Context) ->
|
||||
DeadlineContext = set_deadline(Deadline, Context),
|
||||
Retry = get_service_retry(Service, Method),
|
||||
call_service(Service, Method, Args, ClientOpts, DeadlineContext, Retry).
|
||||
|
||||
call_service(Service, Method, Args, ClientOpts, Context, Retry) ->
|
||||
Request = {get_service_modname(Service), Method, Args},
|
||||
try
|
||||
@ -184,7 +183,7 @@ apply_retry_step({wait, Timeout, Retry}, Deadline0, Error) ->
|
||||
false ->
|
||||
ok = timer:sleep(Timeout),
|
||||
Retry
|
||||
end.
|
||||
end.
|
||||
|
||||
get_service_client_config(ServiceName) ->
|
||||
ServiceClients = genlib_app:env(shortener, service_clients, #{}),
|
||||
@ -200,24 +199,27 @@ get_service_modname(automaton) ->
|
||||
-type signal() :: mg_proto_state_processing_thrift:'SignalArgs'().
|
||||
-type signal_result() :: mg_proto_state_processing_thrift:'SignalResult'().
|
||||
|
||||
-spec handle_function
|
||||
('ProcessSignal', {signal()}, ctx(), woody:options()) ->
|
||||
{ok, signal_result()} | no_return().
|
||||
|
||||
-spec handle_function('ProcessSignal', {signal()}, ctx(), woody:options()) -> {ok, signal_result()} | no_return().
|
||||
handle_function(Func, Args, Ctx, _Opts) ->
|
||||
scoper:scope(machine,
|
||||
scoper:scope(
|
||||
machine,
|
||||
fun() -> handle_function(Func, Args, Ctx) end
|
||||
).
|
||||
|
||||
handle_function('ProcessSignal', {
|
||||
handle_function(
|
||||
'ProcessSignal',
|
||||
{
|
||||
#mg_stateproc_SignalArgs{
|
||||
signal = {Type, Signal},
|
||||
machine = #mg_stateproc_Machine{id = ID, history = History0} = Machine
|
||||
}
|
||||
}, Ctx) ->
|
||||
},
|
||||
Ctx
|
||||
) ->
|
||||
ok = scoper:add_meta(#{id => ID, signal => Type}),
|
||||
History = unmarshal_history(History0),
|
||||
Result = case Signal of
|
||||
Result =
|
||||
case Signal of
|
||||
#mg_stateproc_InitSignal{arg = Args} ->
|
||||
handle_init(unmarshal(term, Args), Ctx);
|
||||
#mg_stateproc_TimeoutSignal{} ->
|
||||
@ -286,7 +288,6 @@ apply_event({created, Slug}, undefined) ->
|
||||
|
||||
marshal(event, {created, #{source := Source, expires_at := ExpiresAt, owner := Owner}}) ->
|
||||
{arr, [{i, 2}, marshal(string, Source), marshal(timestamp, ExpiresAt), marshal(string, Owner)]};
|
||||
|
||||
marshal(timestamp, V) ->
|
||||
marshal(string, V);
|
||||
marshal(string, V) ->
|
||||
@ -306,7 +307,6 @@ unmarshal(event, {arr, [{i, 1}, Source, ExpiresAt]}) ->
|
||||
expires_at => unmarshal(timestamp, ExpiresAt),
|
||||
owner => undefined
|
||||
}};
|
||||
|
||||
unmarshal(timestamp, V) ->
|
||||
unmarshal(string, V);
|
||||
unmarshal(string, {str, V}) ->
|
||||
|
@ -8,7 +8,6 @@
|
||||
-define(DEFAULT_PORT, 8080).
|
||||
|
||||
-spec child_spec(module(), map(), cowboy_router:routes()) -> supervisor:child_spec().
|
||||
|
||||
child_spec(LogicHandler, Opts, AdditionalRoutes) ->
|
||||
{Transport, TransportOpts} = get_socket_transport(Opts),
|
||||
CowboyOpts = get_cowboy_config(LogicHandler, AdditionalRoutes, Opts),
|
||||
@ -52,14 +51,16 @@ get_cowboy_config(LogicHandler, AdditionalRoutes, Opts) ->
|
||||
).
|
||||
|
||||
squash_routes(Routes) ->
|
||||
orddict:to_list(lists:foldl(
|
||||
fun ({K, V}, D) -> orddict:update(K, fun (V0) -> V0 ++ V end, V, D) end,
|
||||
orddict:to_list(
|
||||
lists:foldl(
|
||||
fun({K, V}, D) -> orddict:update(K, fun(V0) -> V0 ++ V end, V, D) end,
|
||||
orddict:new(),
|
||||
Routes
|
||||
)).
|
||||
)
|
||||
).
|
||||
|
||||
mk_operation_id_getter(#{env := Env}) ->
|
||||
fun (Req) ->
|
||||
fun(Req) ->
|
||||
case cowboy_router:execute(Req, Env) of
|
||||
{ok, _, #{handler_opts := {_Operations, _LogicHandler, _SwaggerHandlerOpts} = HandlerOpts}} ->
|
||||
case swag_server_utils:get_operation_id(Req, HandlerOpts) of
|
||||
|
@ -46,7 +46,6 @@ all() ->
|
||||
].
|
||||
|
||||
-spec groups() -> [{atom(), list(), [test_case_name()]}].
|
||||
|
||||
groups() ->
|
||||
[
|
||||
{general, [], [
|
||||
@ -91,20 +90,21 @@ init_per_suite(C) ->
|
||||
] ++ C.
|
||||
|
||||
-spec init_per_group(atom(), config()) -> config().
|
||||
|
||||
init_per_group(_Group, C) ->
|
||||
ShortenerApp =
|
||||
genlib_app:start_application_with(shortener, get_app_config(
|
||||
genlib_app:start_application_with(
|
||||
shortener,
|
||||
get_app_config(
|
||||
?config(port, C),
|
||||
?config(netloc, C),
|
||||
get_keysource("keys/local/private.pem", C)
|
||||
)),
|
||||
)
|
||||
),
|
||||
[
|
||||
{shortener_app, ShortenerApp}
|
||||
] ++ C.
|
||||
|
||||
-spec end_per_group(atom(), config()) -> _.
|
||||
|
||||
end_per_group(_Group, C) ->
|
||||
genlib_app:stop_unload_applications(?config(shortener_app, C)).
|
||||
|
||||
@ -115,13 +115,11 @@ get_keysource(Key, C) ->
|
||||
end_per_suite(C) ->
|
||||
genlib_app:stop_unload_applications(?config(suite_apps, C)).
|
||||
|
||||
-spec init_per_testcase(test_case_name(), config()) ->
|
||||
config().
|
||||
-spec init_per_testcase(test_case_name(), config()) -> config().
|
||||
init_per_testcase(_Name, C) ->
|
||||
C.
|
||||
|
||||
-spec end_per_testcase(test_case_name(), config()) ->
|
||||
config().
|
||||
-spec end_per_testcase(test_case_name(), config()) -> config().
|
||||
end_per_testcase(_Name, _C) ->
|
||||
ok.
|
||||
|
||||
@ -217,14 +215,13 @@ always_unique_url(C) ->
|
||||
N = 42,
|
||||
Params = construct_params(<<"https://oops.io/">>, 3600),
|
||||
{IDs, ShortUrls} = lists:unzip([
|
||||
{ID, ShortUrl} ||
|
||||
_ <- lists:seq(1, N),
|
||||
{ID, ShortUrl}
|
||||
|| _ <- lists:seq(1, N),
|
||||
{ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} <- [shorten_url(Params, C1)]
|
||||
]),
|
||||
N = length(lists:usort(IDs)),
|
||||
N = length(lists:usort(ShortUrls)).
|
||||
|
||||
|
||||
%% cors
|
||||
-spec unsupported_cors_method(config()) -> _.
|
||||
-spec supported_cors_method(config()) -> _.
|
||||
@ -251,24 +248,32 @@ supported_cors_method(C) ->
|
||||
{_, Returned} = lists:keyfind(<<"access-control-allow-methods">>, 1, Headers),
|
||||
Allowed = binary:split(Returned, <<",">>, [global]).
|
||||
|
||||
|
||||
supported_cors_header(C) ->
|
||||
SourceUrl = <<"https://oops.io/">>,
|
||||
Params = construct_params(SourceUrl),
|
||||
C1 = set_api_auth_token(supported_cors_header, [read, write], C),
|
||||
{ok, 201, _, #{<<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C1),
|
||||
ReqHeaders = [{<<"origin">>, <<"localhost">>}, {<<"access-control-request-method">>, <<"GET">>}, {<<"access-control-request-headers">>, <<"content-type,authorization">>}],
|
||||
ReqHeaders = [
|
||||
{<<"origin">>, <<"localhost">>},
|
||||
{<<"access-control-request-method">>, <<"GET">>},
|
||||
{<<"access-control-request-headers">>, <<"content-type,authorization">>}
|
||||
],
|
||||
{ok, 200, Headers, _} = hackney:request(options, ShortUrl, ReqHeaders),
|
||||
{Allowed, _} = shortener_cors_policy:allowed_headers(undefined, undefined),
|
||||
{_, Returned} = lists:keyfind(<<"access-control-allow-headers">>, 1, Headers),
|
||||
[_ | Allowed] = binary:split(Returned, <<",">>, [global]). % truncate origin
|
||||
% truncate origin
|
||||
[_ | Allowed] = binary:split(Returned, <<",">>, [global]).
|
||||
|
||||
unsupported_cors_header(C) ->
|
||||
SourceUrl = <<"https://oops.io/">>,
|
||||
Params = construct_params(SourceUrl),
|
||||
C1 = set_api_auth_token(unsupported_cors_header, [read, write], C),
|
||||
{ok, 201, _, #{<<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C1),
|
||||
ReqHeaders = [{<<"origin">>, <<"localhost">>}, {<<"access-control-request-method">>, <<"GET">>}, {<<"access-control-request-headers">>, <<"content-type,42">>}],
|
||||
ReqHeaders = [
|
||||
{<<"origin">>, <<"localhost">>},
|
||||
{<<"access-control-request-method">>, <<"GET">>},
|
||||
{<<"access-control-request-headers">>, <<"content-type,42">>}
|
||||
],
|
||||
{ok, 200, Headers, _} = hackney:request(options, ShortUrl, ReqHeaders),
|
||||
false = lists:member(<<"access-control-allow-headers">>, Headers),
|
||||
false = lists:member(<<"access-control-allow-credentials">>, Headers),
|
||||
@ -286,14 +291,16 @@ construct_params(SourceUrl, Lifetime) ->
|
||||
|
||||
%%
|
||||
-spec woody_timeout_test(config()) -> _.
|
||||
|
||||
woody_timeout_test(C) ->
|
||||
Apps = genlib_app:start_application_with(shortener, get_app_config(
|
||||
Apps = genlib_app:start_application_with(
|
||||
shortener,
|
||||
get_app_config(
|
||||
?config(port, C),
|
||||
?config(netloc, C),
|
||||
get_keysource("keys/local/private.pem", C),
|
||||
<<"http://invalid_url:8022/v1/automaton">>
|
||||
)),
|
||||
)
|
||||
),
|
||||
C2 = set_api_auth_token(woody_timeout_test, [read, write], C),
|
||||
SourceUrl = <<"https://example.com/">>,
|
||||
Params = construct_params(SourceUrl),
|
||||
@ -306,13 +313,15 @@ woody_timeout_test(C) ->
|
||||
|
||||
%%
|
||||
-spec health_check_passing(config()) -> _.
|
||||
|
||||
health_check_passing(C) ->
|
||||
Apps = genlib_app:start_application_with(shortener, get_app_config(
|
||||
Apps = genlib_app:start_application_with(
|
||||
shortener,
|
||||
get_app_config(
|
||||
?config(port, C),
|
||||
?config(netloc, C),
|
||||
get_keysource("keys/local/private.pem", C)
|
||||
)),
|
||||
)
|
||||
),
|
||||
Path = ?config(api_endpoint, C) ++ "/health",
|
||||
{ok, 200, _, Payload} = hackney:request(get, Path, [], <<>>, [with_body]),
|
||||
#{<<"service">> := <<"shortener">>} = jsx:decode(Payload, [return_maps]),
|
||||
@ -352,15 +361,22 @@ get_shortened_url(ID, C) ->
|
||||
).
|
||||
|
||||
append_common_params(Params, C) ->
|
||||
append_media_type(append_auth(append_request_id(
|
||||
append_media_type(
|
||||
append_auth(
|
||||
append_request_id(
|
||||
maps:merge(#{binding => #{}, qs_val => #{}, header => #{}, body => #{}}, Params)
|
||||
), C)).
|
||||
),
|
||||
C
|
||||
)
|
||||
).
|
||||
|
||||
append_media_type(Params = #{header := Headers}) ->
|
||||
Params#{header => Headers#{
|
||||
Params#{
|
||||
header => Headers#{
|
||||
<<"Accept">> => <<"application/json">>,
|
||||
<<"Content-Type">> => <<"application/json; charset=utf-8">>
|
||||
}}.
|
||||
}
|
||||
}.
|
||||
|
||||
append_auth(Params = #{header := Headers}, C) ->
|
||||
case lists:keyfind(api_auth_token, 1, C) of
|
||||
@ -383,8 +399,8 @@ get_app_config(Port, Netloc, PemFile) ->
|
||||
|
||||
get_app_config(Port, Netloc, PemFile, AutomatonUrl) ->
|
||||
[
|
||||
{space_size , 8},
|
||||
{hash_algorithm , sha256},
|
||||
{space_size, 8},
|
||||
{hash_algorithm, sha256},
|
||||
{api, #{
|
||||
ip => "::",
|
||||
port => Port,
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 540183862bc9fd04682e226de2056a320fd44be9
|
||||
Subproject commit f42e059d9ec93826ba4ad23232eed8ce67bd5486
|
@ -134,5 +134,11 @@
|
||||
]}.
|
||||
|
||||
{plugins, [
|
||||
rebar3_run
|
||||
{rebar3_run, "0.3.0"},
|
||||
{erlfmt, "0.8.0"}
|
||||
]}.
|
||||
|
||||
{erlfmt, [
|
||||
{print_width, 120},
|
||||
{files, "apps/shortener/{src,include,test}/*.{hrl,erl}"}
|
||||
]}.
|
||||
|
Loading…
Reference in New Issue
Block a user