Current section

Files

Jump to
swidden src swidden_api_handler.erl
Raw

src/swidden_api_handler.erl

-module(swidden_api_handler).
-export([init/2,
terminate/3]).
-include("swidden.hrl").
%% > re:run(<<"XYZ_20001210.CreateUser">>, <<"^([a-zA-Z]+)\_(\\d{4}\\d{2}\\d{2})\.([a-zA-Z]+)$">>, [{capture, all_but_first, binary}]).
%% {match,[<<"XYZ">>,<<"20001210">>,<<"CreateUser">>]}
-define(REGEXP, <<"^([a-zA-Z]+)\_(\\d{4}\\d{2}\\d{2})\.([a-zA-Z]+)$">>).
-define(DEFAULT_HEADERS, #{<<"content-type">> => <<"application/json">>}).
-define(REDIRECT_STATUS_CODE, 307).
%% Cowboy read_body/2 の length オプションのデフォルト(1 回あたりの chunk サイズ)
-define(READ_BODY_CHUNK_SIZE, 8000000).
%% リクエスト Body 全体の上限(Cowboy は chunk 単位の length しか持たないため自前で enforce する)
-define(MAX_BODY_SIZE, 8000000).
-type state() :: cowboy_handler:opts().
-spec init(cowboy_req:req(), state()) -> {ok, cowboy_req:req(), state()}.
init(Req, Opts) ->
HeaderName = proplists:get_value(header_name, Opts),
Services = proplists:get_value(services, Opts),
Interceptor = proplists:get_value(interceptor, Opts),
case cowboy_req:method(Req) of
<<"POST">> ->
case cowboy_req:header(HeaderName, Req) of
undefined ->
%% ヘッダーがみつからない
RawJSON = jsone:encode(#{type => <<"MissingHeaderName">>}, [skip_undefined]),
Req2 = cowboy_req:reply(400, ?DEFAULT_HEADERS, RawJSON, Req),
{ok, Req2, Opts};
HeaderValue ->
%% Service_Version.Operation として分解する
case re:run(HeaderValue, ?REGEXP, [{capture, all_but_first, binary}]) of
{match, [Service, Version, Operation]} ->
case lists:member(Service, Services) of
true ->
Req2 = handle(Service, Version, Operation, Req, Interceptor),
{ok, Req2, Opts};
false when Services == [] ->
Req2 = handle(Service, Version, Operation, Req, Interceptor),
{ok, Req2, Opts};
false ->
Req2 = cowboy_req:reply(400,
?DEFAULT_HEADERS,
jsone:encode(#{error_type => <<"InvalidTarget">>},
[skip_undefined]),
Req),
{ok, Req2, Opts}
end;
_ ->
%% サービスに対応してなかったよ
Req2 = cowboy_req:reply(400,
?DEFAULT_HEADERS,
jsone:encode(#{error_type => <<"MissingService">>},
[skip_undefined]),
Req),
{ok, Req2, Opts}
end
end;
_Other ->
%% POST 以外受け付けていないのでエラーメッセージ
RawJSON = jsone:encode(#{type => <<"UnexpectedMethod">>}, [skip_undefined]),
Req2 = cowboy_req:reply(400, ?DEFAULT_HEADERS, RawJSON, Req),
{ok, Req2, Opts}
end.
read_body(Req) ->
read_body(Req, <<>>, 0).
read_body(Req, Acc, AccSize) ->
case AccSize >= ?MAX_BODY_SIZE of
true ->
{error, payload_too_large};
false ->
%% length は chunk サイズ(best effort)。各 read_body 呼び出しごとに渡す。
ChunkSize = erlang:min(?READ_BODY_CHUNK_SIZE, ?MAX_BODY_SIZE - AccSize),
try cowboy_req:read_body(Req, #{length => ChunkSize}) of
{ok, Data, Req2} ->
%% fin 時は chunk サイズを超える Body が届く(cowboy_stream_h の IsFin =:= fin 分岐)
NewSize = AccSize + byte_size(Data),
case NewSize > ?MAX_BODY_SIZE of
true ->
{error, payload_too_large};
false ->
{ok, iolist_to_binary([Acc, Data]), Req2}
end;
{more, Data, Req2} ->
DataSize = byte_size(Data),
NewSize = AccSize + DataSize,
%% {more, ...} は nofin: これ以上 Body が続く。
%% read_urlencoded_body/2 と同様、chunk が満杯なら上限超過とみなす。
case (NewSize > ?MAX_BODY_SIZE) orelse (DataSize >= ChunkSize) of
true ->
{error, payload_too_large};
false ->
read_body(Req2, [Acc, Data], NewSize)
end
catch
exit:timeout ->
{error, timeout};
exit:{{request_error, payload_too_large, _}, _} ->
{error, payload_too_large}
end
end.
handle(Service, Version, Operation, Req, Interceptor) ->
%% TODO(v); リファクタリング
case cowboy_req:has_body(Req) of
true ->
case read_body(Req) of
{error, payload_too_large} ->
reply_json(413, #{error_type => <<"PayloadTooLarge">>}, Req);
{error, timeout} ->
reply_json(408, #{error_type => <<"RequestTimeout">>}, Req);
{ok, <<>>, Req2} ->
case dispatch(Service, Version, Operation, Interceptor) of
200 ->
cowboy_req:reply(200, ?DEFAULT_HEADERS, [], Req2);
{?REDIRECT_STATUS_CODE, Location} ->
cowboy_req:reply(?REDIRECT_STATUS_CODE, #{<<"location">> => Location}, [], Req2);
{StatusCode, JSON} ->
RawJSON = jsone:encode(JSON, [skip_undefined]),
cowboy_req:reply(StatusCode, ?DEFAULT_HEADERS, RawJSON, Req2)
end;
{ok, Body, Req2} ->
case validate_json(Service, Version, Operation, Body, Interceptor) of
200 ->
cowboy_req:reply(200, ?DEFAULT_HEADERS, [], Req2);
{?REDIRECT_STATUS_CODE, Location} ->
cowboy_req:reply(?REDIRECT_STATUS_CODE, #{<<"location">> => Location}, [], Req2);
{StatusCode, JSON} ->
RawJSON = jsone:encode(JSON, [skip_undefined]),
cowboy_req:reply(StatusCode, ?DEFAULT_HEADERS, RawJSON, Req2)
end
end;
false ->
case dispatch(Service, Version, Operation, Interceptor) of
200 ->
cowboy_req:reply(200, ?DEFAULT_HEADERS, [], Req);
{?REDIRECT_STATUS_CODE, Location} ->
cowboy_req:reply(?REDIRECT_STATUS_CODE, #{<<"location">> => Location}, [], Req);
{StatusCode, JSON} ->
RawJSON = jsone:encode(JSON, [skip_undefined]),
cowboy_req:reply(StatusCode, ?DEFAULT_HEADERS, RawJSON, Req)
end
end.
-spec terminate(term(), cowboy_req:req(), state()) -> ok.
terminate(normal, _Req, _State) ->
ok;
terminate(_Reason, _Req, _State) ->
ok.
dispatch(Service, Version, Operation, Interceptor) ->
case swidden_dispatch:lookup(Service, Version, Operation) of
not_found ->
{400, #{error_type => <<"MissingTarget">>}};
{Module, Function} ->
case code:which(Module) of
non_existing ->
{400, #{error_type => <<"MissingTargetModule">>}};
_ ->
case lists:member({Function, 0}, Module:module_info(exports)) of
true ->
preprocess0(Module, Function, Interceptor);
false ->
{400,
#{
error_type => <<"MissingTargetFunction">>,
error_reason => #{
service => Service,
version => Version,
operation => Operation
}
}}
end
end
end.
validate_json(Service, Version, Operation, RawJSON, Interceptor) ->
case swidden_json_schema:validate_json(Service, Version, Operation, RawJSON) of
{ok, Module, Function, JSON} ->
case code:which(Module) of
non_existing ->
{400, #{error_type => <<"MissingTargetModule">>}};
_ ->
case lists:member({Function, 1}, Module:module_info(exports)) of
true ->
preprocess1(Module, Function, JSON, Interceptor);
false ->
{400,
#{
error_type => <<"MissingTargetFunction">>,
error_reason => #{
service => Service,
version => Version,
operation => Operation
}
}}
end
end;
{error, {data_error, _Reason}} ->
{400, #{error_type => <<"MalformedJSON">>}};
{error, {database_error, _Key, schema_not_found}} ->
%% TODO(v); この部分は外だしする
{400,
#{
error_type => <<"SchemaNotFound">>,
error_reason => #{
service => Service,
version => Version,
operation => Operation
}
}};
{error, Reasons} ->
ErrorReasons = swidden_json_schema:to_json(Reasons),
{400,
#{
error_type => <<"InvalidJSON">>,
error_reason => ErrorReasons
}}
end.
preprocess0(Module, Function, undefined) ->
apply_mfa(Module, Function, []);
preprocess0(Module, Function, Interceptor) ->
run_interceptor(fun() ->
case Interceptor:preprocess(Module, Function) of
continue ->
apply_mfa(Module, Function, [], Interceptor);
{stop, Result} ->
safe_response(Result)
end
end).
preprocess1(Module, Function, JSON, undefined) ->
apply_mfa(Module, Function, [JSON]);
preprocess1(Module, Function, JSON0, Interceptor) ->
run_interceptor(fun() ->
case Interceptor:preprocess(Module, Function, JSON0) of
{continue, JSON1} ->
apply_mfa(Module, Function, [JSON1], Interceptor);
{stop, Result} ->
postprocess(Module, Function, Result, Interceptor)
end
end).
apply_mfa(Module, Function, Args) ->
try
Result = apply(Module, Function, Args),
response(Result)
catch
_Class:_Reason:_Stack ->
{500, #{error_type => <<"HandlerException">>}}
end.
apply_mfa(Module, Function, Args, Interceptor) ->
try
Result = apply(Module, Function, Args),
postprocess(Module, Function, Result, Interceptor)
catch
_Class:_Reason:_Stack ->
{500, #{error_type => <<"HandlerException">>}}
end.
postprocess(Module, Function, Result0, Interceptor) ->
run_interceptor(fun() ->
Result1 = Interceptor:postprocess(Module, Function, Result0),
safe_response(Result1)
end).
run_interceptor(Fun) ->
try Fun() of
Result ->
Result
catch
_Class:_Reason:_Stack ->
handler_exception()
end.
safe_response(Result) ->
try
response(Result)
catch
_Class:_Reason:_Stack ->
handler_exception()
end.
handler_exception() ->
{500, #{error_type => <<"HandlerException">>}}.
reply_json(StatusCode, JSON, Req) ->
RawJSON = jsone:encode(JSON, [skip_undefined]),
cowboy_req:reply(StatusCode, ?DEFAULT_HEADERS, RawJSON, Req).
response({ok, {redirect, Location}}) ->
{?REDIRECT_STATUS_CODE, Location};
response(ok) ->
200;
response({ok, RespJSON}) ->
{200, RespJSON};
response({error, Type}) ->
{400, #{error_type => Type}};
response({error, Type, Reason}) ->
{400, #{error_type => Type, error_reason => Reason}}.