%%%------------------------------------------------------------------- %%% @doc Обработчик конкретного календаря (клиентский API). %%% GET – получить информацию о календаре. %%% PUT – обновить календарь (владельцем). %%% DELETE – удалить календарь (владельцем). %%% @end %%%------------------------------------------------------------------- -module(handler_calendar_by_id). -behaviour(cowboy_handler). -export([init/2]). -export([trails/0]). -include("records.hrl"). %%% cowboy_handler callback -spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. init(Req, _Opts) -> case cowboy_req:method(Req) of <<"GET">> -> get_calendar(Req); <<"PUT">> -> update_calendar(Req); <<"DELETE">> -> delete_calendar(Req); _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) end. %%% Swagger metadata -spec trails() -> [map()]. trails() -> BaseParams = [#{ name => <<"id">>, in => <<"path">>, description => <<"Calendar ID">>, required => true, schema => #{type => string} }], [ #{ % GET path => <<"/v1/calendars/:id">>, method => <<"GET">>, description => <<"Get calendar by ID">>, tags => [<<"Calendars">>], parameters => BaseParams, responses => #{ 200 => #{ description => <<"Calendar details">>, content => #{<<"application/json">> => #{schema => calendar_schema()}} } } }, #{ % PUT path => <<"/v1/calendars/:id">>, method => <<"PUT">>, description => <<"Update calendar">>, tags => [<<"Calendars">>], parameters => BaseParams, requestBody => #{ required => true, content => #{<<"application/json">> => #{schema => calendar_update_schema()}} }, responses => #{ 200 => #{description => <<"Calendar updated">>} } }, #{ % DELETE path => <<"/v1/calendars/:id">>, method => <<"DELETE">>, description => <<"Delete calendar">>, tags => [<<"Calendars">>], parameters => BaseParams, responses => #{ 200 => #{description => <<"Calendar deleted">>} } } ]. -spec calendar_schema() -> map(). calendar_schema() -> #{ type => object, properties => #{ id => #{type => string}, owner_id => #{type => string}, title => #{type => string}, description => #{type => string}, type => #{type => string, enum => [<<"personal">>, <<"commercial">>]}, confirmation => #{type => string, enum => [<<"auto">>, <<"manual">>]}, tags => #{type => array, items => #{type => string}}, settings => #{type => object}, rating_avg => #{type => number, format => float}, rating_count => #{type => integer}, status => #{type => string}, created_at => #{type => string, format => <<"date-time">>}, updated_at => #{type => string, format => <<"date-time">>} } }. -spec calendar_update_schema() -> map(). calendar_update_schema() -> #{ type => object, properties => #{ title => #{type => string}, description => #{type => string}, type => #{type => string}, confirmation => #{type => string}, tags => #{type => array, items => #{type => string}}, short_name => #{type => string}, category => #{type => string}, color => #{type => string}, image_url => #{type => string}, settings => #{type => object, nullable => true} } }. %%% Internal functions get_calendar(Req) -> case handler_utils:auth_user_optional(Req) of {ok, UserId, Req1} -> CalendarId = cowboy_req:binding(id, Req1), case logic_calendar:get_calendar(UserId, CalendarId) of {ok, Calendar} -> Json0 = handler_utils:calendar_to_json(Calendar), Following = UserId =/= <<>> andalso logic_calendar_follow:is_following(UserId, Calendar#calendar.id), handler_utils:send_json(Req1, 200, Json0#{following => Following}); {error, access_denied} -> handler_utils:send_error(Req1, 403, <<"Access denied">>); {error, not_found} -> handler_utils:send_error(Req1, 404, <<"Calendar not found">>); {error, _} -> handler_utils:send_error(Req1, 500, <<"Internal server error">>) end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) end. update_calendar(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> CalendarId = cowboy_req:binding(id, Req1), {ok, Body, Req2} = cowboy_req:read_body(Req1), try jsx:decode(Body, [return_maps]) of UpdatesMap when is_map(UpdatesMap) -> Updates0 = maps:to_list(UpdatesMap), Updates = convert_calendar_fields(Updates0), case logic_calendar:update_calendar(UserId, CalendarId, Updates) of {ok, Calendar} -> Json0 = handler_utils:calendar_to_json(Calendar), Following = logic_calendar_follow:is_following(UserId, CalendarId), handler_utils:send_json(Req2, 200, Json0#{following => Following}); {error, access_denied} -> handler_utils:send_error(Req2, 403, <<"Access denied">>); {error, subscription_required} -> handler_utils:send_error(Req2, 402, <<"Subscription required for commercial calendar">>); {error, default_calendar} -> handler_utils:send_error(Req2, 403, <<"default_calendar">>); {error, not_found} -> handler_utils:send_error(Req2, 404, <<"Calendar not found">>); {error, {invalid_settings, Key}} when is_binary(Key) -> handler_utils:send_error(Req2, 400, <<"Invalid settings.", Key/binary>>); {error, _} -> handler_utils:send_error(Req2, 500, <<"Internal server error">>) end; _ -> handler_utils:send_error(Req2, 400, <<"Invalid JSON">>) catch _:_ -> handler_utils:send_error(Req2, 400, <<"Invalid JSON format">>) end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) end. delete_calendar(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> CalendarId = cowboy_req:binding(id, Req1), case logic_calendar:delete_calendar(UserId, CalendarId) of {ok, _} -> handler_utils:send_json(Req1, 200, #{status => <<"deleted">>}); {error, default_calendar} -> handler_utils:send_error(Req1, 403, <<"default_calendar">>); {error, access_denied} -> handler_utils:send_error(Req1, 403, <<"Access denied">>); {error, not_found} -> handler_utils:send_error(Req1, 404, <<"Calendar not found">>); {error, _} -> handler_utils:send_error(Req1, 500, <<"Internal server error">>) end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) end. %% @private Преобразует бинарные ключи и значения в атомы для обновлений календаря. -spec convert_calendar_fields([{binary(), term()}]) -> [{atom(), term()}]. convert_calendar_fields(Updates) -> lists:map(fun convert_field/1, Updates). -spec convert_field({binary(), term()}) -> {atom(), term()}. convert_field({<<"title">>, Val}) -> {title, Val}; convert_field({<<"description">>, Val}) -> {description, Val}; convert_field({<<"type">>, <<"personal">>}) -> {type, personal}; convert_field({<<"type">>, <<"commercial">>}) -> {type, commercial}; convert_field({<<"type">>, Val}) -> {type, Val}; convert_field({<<"confirmation">>, <<"auto">>}) -> {confirmation, auto}; convert_field({<<"confirmation">>, <<"manual">>}) -> {confirmation, manual}; convert_field({<<"confirmation">>, #{<<"timeout">> := N}}) when is_integer(N), N > 0 -> {confirmation, {timeout, N}}; convert_field({<<"confirmation">>, Val}) -> {confirmation, Val}; convert_field({<<"tags">>, Val}) -> {tags, Val}; convert_field({<<"short_name">>, Val}) -> {short_name, Val}; convert_field({<<"category">>, Val}) -> {category, Val}; convert_field({<<"color">>, Val}) -> {color, Val}; convert_field({<<"image_url">>, Val}) -> {image_url, Val}; convert_field({<<"settings">>, Val}) when is_map(Val) -> {settings, Val}; convert_field(Other) -> Other.