Add formatter (#33)

* Add erlfmt
* Update build-utils
* Apply erlfmt
* Exclude generated swag client and server from format check
This commit is contained in:
Andrey Fadeev 2020-10-13 17:09:27 +03:00 committed by GitHub
parent 8e89bdf915
commit f0cd85e458
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 305 additions and 300 deletions

View File

@ -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

View File

@ -4,7 +4,7 @@
%% Application callbacks
-export([start/2]).
-export([stop /1]).
-export([stop/1]).
-behaviour(supervisor).
@ -17,33 +17,29 @@
%%
-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, {
{one_for_all, 0, 1},
% TODO
get_processor_childspecs(genlib_app:env(?MODULE, processor), HealthRoutes) ++
get_api_childspecs(genlib_app:env(?MODULE, api), HealthRoutes)
{one_for_all, 0, 1},
% TODO
get_processor_childspecs(genlib_app:env(?MODULE, processor), HealthRoutes) ++
get_api_childspecs(genlib_app:env(?MODULE, api), HealthRoutes)
}}.
get_health_routes(Check) ->
@ -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,23 +55,25 @@ enable_health_logging(Check = #{}) ->
get_processor_childspecs(Opts, HealthRoutes) ->
{ok, IP} = inet:parse_address(maps:get(ip, Opts, "::")),
[woody_server:child_spec(
?MODULE,
#{
ip => IP,
port => maps:get(port, Opts, 8022),
protocol_opts => maps:get(protocol_opts, Opts, #{}),
transport_opts => maps:get(transport_opts, Opts, #{}),
event_handler => scoper_woody_event_handler,
handlers => [
{"/v1/stateproc", {
{mg_proto_state_processing_thrift, 'Processor'},
shortener_slug
}}
],
additional_routes => HealthRoutes
}
)].
[
woody_server:child_spec(
?MODULE,
#{
ip => IP,
port => maps:get(port, Opts, 8022),
protocol_opts => maps:get(protocol_opts, Opts, #{}),
transport_opts => maps:get(transport_opts, Opts, #{}),
event_handler => scoper_woody_event_handler,
handlers => [
{"/v1/stateproc", {
{mg_proto_state_processing_thrift, 'Processor'},
shortener_slug
}}
],
additional_routes => HealthRoutes
}
)
].
get_api_childspecs(Opts, HealthRoutes) ->
AuthorizerSpec = shortener_authorizer_jwt:get_child_spec(maps:get(authorizer, Opts)),

View File

@ -2,13 +2,13 @@
%%
-opaque t() :: [{{priority(), scope()}, [permission()]}].
-opaque t() :: [{{priority(), scope()}, [permission()]}].
-type priority() :: integer().
-type scope() :: [resource() | {resource(), resource_id()}, ...].
-type resource() :: atom().
-type resource_id() :: binary().
-type permission() :: read | write.
-type priority() :: integer().
-type scope() :: [resource() | {resource(), resource_id()}, ...].
-type resource() :: atom().
-type resource_id() :: binary().
-type permission() :: read | write.
-export_type([t/0]).
-export_type([scope/0]).
@ -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(),

View File

@ -4,14 +4,12 @@
-export([authorize_operation/3]).
-type context() :: shortener_authorizer_jwt:t().
-type claims() :: shortener_authorizer_jwt:claims().
-type claims() :: shortener_authorizer_jwt:claims().
-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}) ->

View File

@ -19,18 +19,18 @@
-include_lib("jose/include/jose_jwk.hrl").
-include_lib("jose/include/jose_jwt.hrl").
-type keyname() :: term().
-type kid() :: binary().
-type key() :: #jose_jwk{}.
-type token() :: binary().
-type claims() :: #{binary() => term()}.
-type subject() :: {subject_id(), shortener_acl:t()}.
-type keyname() :: term().
-type kid() :: binary().
-type key() :: #jose_jwk{}.
-type token() :: binary().
-type claims() :: #{binary() => term()}.
-type subject() :: {subject_id(), shortener_acl:t()}.
-type subject_id() :: binary().
-type t() :: {subject(), claims()}.
-type expiration() ::
{lifetime, Seconds :: pos_integer()} |
{deadline, UnixTs :: pos_integer()} |
unlimited.
-type t() :: {subject(), claims()}.
-type expiration() ::
{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),
@ -120,14 +116,12 @@ select_signee(error, _KeyInfos) ->
%%
-type keyinfo() :: #{
kid => kid(),
sign => boolean(),
kid => kid(),
sign => boolean(),
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{} ->
@ -158,8 +151,8 @@ store_key(Keyname, {pem_file, Filename}, Opts) ->
get_key_info(#{kid := KID, signer := Signer, verifier := Verifier}) ->
#{
kid => KID,
sign => Signer /= undefined,
kid => KID,
sign => Signer /= undefined,
verify => Verifier /= undefined
}.
@ -168,20 +161,27 @@ derive_kid(JWK, #{kid := DeriveFun}) when is_function(DeriveFun, 1) ->
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
jwk => JWK,
kid => KID,
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 = #{
<<"resource_access">> := #{
<<"url-shortener">> := #{
<<"roles">> := Roles
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}}).
@ -358,7 +357,7 @@ decode_roles(_) ->
insert_key(Keyname, Key = #{kid := KID}) ->
insert_values(#{
{keyname, Keyname} => Key,
{kid, KID} => Key
{kid, KID} => Key
}).
get_key_by_name(Keyname) ->
@ -379,6 +378,7 @@ get_signee_key() ->
undefined ->
undefined
end.
%%
base64url_to_map(V) when is_binary(V) ->

View File

@ -1,4 +1,5 @@
-module(shortener_cors_policy).
-behaviour(cowboy_cors_policy).
-export([policy_init/1]).
@ -7,28 +8,25 @@
-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">>,
<<"access-control-allow-headers">>,
<<"authorization">>,
<<"content-type">>,
<<"x-request-id">>,
<<"x-requested-with">>
], State}.
<<"accept">>,
<<"access-control-allow-headers">>,
<<"authorization">>,
<<"content-type">>,
<<"x-request-id">>,
<<"x-requested-with">>
],
State}.
-spec allowed_methods(cowboy_req:req(), any()) -> {[binary()], any()}.
allowed_methods(_, State) ->
{[<<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], State}.

View File

@ -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]).
@ -18,20 +20,17 @@
-define(REALM, <<"external">>).
-type operation_id() :: swag_server:operation_id().
-type request_ctx() :: swag_server:request_context().
-type request_ctx() :: swag_server:request_context().
-type request_data() :: #{atom() | binary() => term()}.
-type subject_id() :: woody_user_identity:id().
-type subject_id() :: woody_user_identity:id().
-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} ->
@ -70,9 +68,9 @@ create_woody_ctx(#{'X-Request-ID' := RequestID}, AuthContext) ->
collect_user_identity(AuthContext) ->
genlib_map:compact(#{
id => get_subject_id(AuthContext),
realm => ?REALM,
email => get_claim(<<"email">>, AuthContext, undefined),
id => get_subject_id(AuthContext),
realm => ?REALM,
email => get_claim(<<"email">>, AuthContext, undefined),
username => get_claim(<<"name">>, AuthContext, undefined)
}).
@ -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' := #{
<<"sourceUrl">> := SourceUrl,
<<"expiresAt">> := ExpiresAt
}},
#{
'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, #{}, #{
<<"code">> => <<"forbidden_source_url">>,
<<"message">> => <<"Source URL is forbidden">>
}}}
{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()
).
@ -199,32 +197,29 @@ get_source_url_whitelist() ->
%%
-type state() :: undefined.
-type request() :: cowboy_req:req().
-type state() :: undefined.
-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
{ok, #{source := Source, expires_at := ExpiresAt}} ->
Seconds = genlib_rfc3339:parse(ExpiresAt, second),
{Date, Time} = calendar:system_time_to_universal_time(Seconds, second),
Headers = #{
<<"location">> => Source,
<<"expires">> => cowboy_clock:rfc1123({Date, Time}),
<<"cache-control">> => <<"must-revalidate">>
},
cowboy_req:reply(301, Headers, Req);
{error, notfound} ->
cowboy_req:reply(404, Req)
end,
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),
Headers = #{
<<"location">> => Source,
<<"expires">> => cowboy_clock:rfc1123({Date, Time}),
<<"cache-control">> => <<"must-revalidate">>
},
cowboy_req:reply(301, Headers, Req);
{error, notfound} ->
cowboy_req:reply(404, Req)
end,
{ok, Req1, Opts}.
-spec terminate(terminate_reason(), request(), state()) ->
ok.
-spec terminate(terminate_reason(), request(), state()) -> ok.
terminate(_Reason, _Req, _St) ->
ok.

View File

@ -1,4 +1,5 @@
-module(shortener_slug).
-include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl").
%% API
@ -18,28 +19,27 @@
%%
-type timestamp() :: binary(). % RFC 3339
% RFC 3339
-type timestamp() :: binary().
-type id() :: binary().
-type source() :: binary().
-type owner() :: binary().
-type id() :: binary().
-type source() :: binary().
-type owner() :: binary().
-type expiration() :: timestamp().
-type slug() :: #{
id => id(),
source => source(),
owner => owner() | undefined,
expires_at => expiration()
-type slug() :: #{
id => id(),
source => source(),
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().
-type ctx() :: woody_context:ctx().
-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, #{}),
@ -197,34 +196,37 @@ get_service_modname(automaton) ->
%%
-type signal() :: mg_proto_state_processing_thrift:'SignalArgs'().
-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', {
#mg_stateproc_SignalArgs{
signal = {Type, Signal},
machine = #mg_stateproc_Machine{id = ID, history = History0} = Machine
}
}, Ctx) ->
handle_function(
'ProcessSignal',
{
#mg_stateproc_SignalArgs{
signal = {Type, Signal},
machine = #mg_stateproc_Machine{id = ID, history = History0} = Machine
}
},
Ctx
) ->
ok = scoper:add_meta(#{id => ID, signal => Type}),
History = unmarshal_history(History0),
Result = case Signal of
#mg_stateproc_InitSignal{arg = Args} ->
handle_init(unmarshal(term, Args), Ctx);
#mg_stateproc_TimeoutSignal{} ->
handle_timeout(collapse_history(History), Ctx);
#mg_stateproc_RepairSignal{arg = Args} ->
handle_repair(unmarshal(term, Args), collapse_history(History), Ctx)
end,
Result =
case Signal of
#mg_stateproc_InitSignal{arg = Args} ->
handle_init(unmarshal(term, Args), Ctx);
#mg_stateproc_TimeoutSignal{} ->
handle_timeout(collapse_history(History), Ctx);
#mg_stateproc_RepairSignal{arg = Args} ->
handle_repair(unmarshal(term, Args), collapse_history(History), Ctx)
end,
{ok, handle_signal_result(Result, Machine)}.
handle_signal_result(Result, Machine) ->
@ -235,7 +237,7 @@ handle_signal_result(Result, Machine) ->
construct_machine_change(Events, #mg_stateproc_Machine{aux_state = AuxState}) ->
#mg_stateproc_MachineStateChange{
events = [construct_content(marshal(event, E)) || E <- Events],
events = [construct_content(marshal(event, E)) || E <- Events],
aux_state = construct_aux_state(AuxState)
}.
@ -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}) ->

View File

@ -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),
@ -22,17 +21,17 @@ child_spec(LogicHandler, Opts, AdditionalRoutes) ->
get_socket_transport(Opts) ->
{ok, IP} = inet:parse_address(maps:get(ip, Opts, ?DEFAULT_IP_ADDR)),
Port = maps:get(port, Opts, ?DEFAULT_PORT),
Port = maps:get(port, Opts, ?DEFAULT_PORT),
Acceptors = maps:get(acceptors, Opts, ?DEFAULT_ACCEPTORS_POOLSIZE),
{ranch_tcp, #{socket_opts => [{ip, IP}, {port, Port}], num_acceptors => Acceptors}}.
{ranch_tcp, #{socket_opts => [{ip, IP}, {port, Port}], num_acceptors => Acceptors}}.
get_cowboy_config(LogicHandler, AdditionalRoutes, Opts) ->
ShortUrlTemplate = maps:get(short_url_template, Opts),
ShortUrlPath = maps:get(path, ShortUrlTemplate),
Routes = squash_routes(
AdditionalRoutes ++
swag_server_router:get_paths(LogicHandler) ++
[{'_', [{genlib:to_list(ShortUrlPath) ++ ":shortenedUrlID", shortener_handler, #{}}]}]
swag_server_router:get_paths(LogicHandler) ++
[{'_', [{genlib:to_list(ShortUrlPath) ++ ":shortenedUrlID", shortener_handler, #{}}]}]
),
CowboyOps = #{
env => #{
@ -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:new(),
Routes
)).
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
@ -71,4 +72,4 @@ mk_operation_id_getter(#{env := Env}) ->
_ ->
#{}
end
end.
end.

View File

@ -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(
?config(port, C),
?config(netloc, C),
get_keysource("keys/local/private.pem", C)
)),
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),
{ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} <- [shorten_url(Params, C1)]
{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(
?config(port, C),
?config(netloc, C),
get_keysource("keys/local/private.pem", C),
<<"http://invalid_url:8022/v1/automaton">>
)),
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(
?config(port, C),
?config(netloc, C),
get_keysource("keys/local/private.pem", C)
)),
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(
maps:merge(#{binding => #{}, qs_val => #{}, header => #{}, body => #{}}, Params)
), C)).
append_media_type(
append_auth(
append_request_id(
maps:merge(#{binding => #{}, qs_val => #{}, header => #{}, body => #{}}, Params)
),
C
)
).
append_media_type(Params = #{header := Headers}) ->
Params#{header => Headers#{
<<"Accept">> => <<"application/json">>,
<<"Content-Type">> => <<"application/json; charset=utf-8">>
}}.
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,15 +399,15 @@ 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,
authorizer => #{
signee => local,
ip => "::",
port => Port,
authorizer => #{
signee => local,
keyset => #{
local => {pem_file, PemFile}
local => {pem_file, PemFile}
}
},
source_url_whitelist => [
@ -400,14 +416,14 @@ get_app_config(Port, Netloc, PemFile, AutomatonUrl) ->
"http://localhost/*"
],
short_url_template => #{
scheme => http,
netloc => Netloc,
path => "/r/e/d/i/r/"
scheme => http,
netloc => Netloc,
path => "/r/e/d/i/r/"
}
}},
{processor, #{
ip => "::",
port => 8022
ip => "::",
port => 8022
}},
{health_check, #{
service => {erl_health, service, [<<"shortener">>]}
@ -421,10 +437,10 @@ get_app_config(Port, Netloc, PemFile, AutomatonUrl) ->
% default value is 'finish'
% for more info look genlib_retry :: strategy()
% https://github.com/rbkmoney/genlib/blob/master/src/genlib_retry.erl#L19
'Start' => {linear, 3, 1000},
'GetMachine' => {linear, 3, 1000},
'Remove' => {linear, 3, 1000},
'_' => finish
'Start' => {linear, 3, 1000},
'GetMachine' => {linear, 3, 1000},
'Remove' => {linear, 3, 1000},
'_' => finish
}
}
}}

@ -1 +1 @@
Subproject commit 540183862bc9fd04682e226de2056a320fd44be9
Subproject commit f42e059d9ec93826ba4ad23232eed8ce67bd5486

View File

@ -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}"}
]}.