rest_cowboy.erl 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. -module(rest_cowboy).
  2. -author('Dmitry Bushmelev').
  3. -record(st, {resource_module = undefined :: atom(), resource_id = undefined :: binary()}).
  4. -export([init/2, rest_init/2, resource_exists/2, allowed_methods/2, content_types_provided/2,
  5. to_html/2, to_json/2, content_types_accepted/2, delete_resource/2,
  6. handle_urlencoded_data/2, handle_json_data/2]).
  7. init(Req, Opts) -> {cowboy_rest, Req, Opts}.
  8. -ifndef(REST_JSON).
  9. -define(REST_JSON, (application:get_env(rest,json,jsone))).
  10. -endif.
  11. c(X) -> list_to_atom(binary_to_list(X)).
  12. rest_init(Req, _Opts) ->
  13. {Resource, Req1} = cowboy_req:binding(resource, Req),
  14. Module = case rest_module(Resource) of {ok, M} -> M; _ -> undefined end,
  15. {Id, Req2} = cowboy_req:binding(id, Req1),
  16. {Origin, Req3} = cowboy_req:header(<<"origin">>, Req2, <<"*">>),
  17. Req4 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Origin">>, Origin, Req3),
  18. io:format("REST INIT~p"),
  19. {ok, Req4, #st{resource_module = Module, resource_id = Id}}.
  20. resource_exists(#{bindings := #{resource := Module, id := Id}} = Req, State) ->
  21. M = c(Module),
  22. io:format("EXISTS: ~p dymamic: ~p~n",[Id,M:exists(Id)]),
  23. {M:exists(Id), Req, State};
  24. resource_exists(#{bindings := #{resource := Module}} = Req, State) -> io:format("resource ~p: no-id~n",[Module]), {true, Req, State};
  25. resource_exists(#{bindings := #{id := _}} = Req, State) -> io:format("EXISTS id: true~n"), {true, Req, State}.
  26. allowed_methods(#{bindings := #{resource := _}} = Req, State) -> {[<<"GET">>, <<"POST">>], Req, State};
  27. allowed_methods(#{bindings := #{resource := _, id := _}} = Req, State) -> {[<<"GET">>, <<"PUT">>, <<"DELETE">>], Req, State}.
  28. content_types_provided(#{bindings := #{resource := Module}} = Req, State) ->
  29. {case erlang:function_exported(c(Module), to_html, 1) of
  30. false -> [{<<"application/json">>, to_json}];
  31. true -> [{<<"text/html">>, to_html}, {<<"application/json">>, to_json}] end,
  32. Req, State}.
  33. to_html(#{bindings := #{resource := Module, id := Id}} = Req, State) ->
  34. M = c(Module),
  35. Body = case Id of
  36. undefined -> [M:to_html(Resource) || Resource <- M:get()];
  37. _ -> M:to_html(M:get(Id)) end,
  38. Html = case erlang:function_exported(M, html_layout, 2) of
  39. true -> M:html_layout(Req, Body);
  40. false -> default_html_layout(Body) end,
  41. {Html, Req, State}.
  42. default_html_layout(Body) -> [<<"<html><body>">>, Body, <<"</body></html>">>].
  43. to_json(#{bindings := #{resource := Module, id := Id}} = Req, State) ->
  44. io:format("~p ~p ~p~n",[?FUNCTION_NAME,Module,Id]),
  45. M = c(Module),
  46. Struct = case Id of
  47. undefined -> [{M, [ M:to_json(Resource) || Resource <- M:get() ] } ];
  48. _ -> M:to_json(M:get(Id)) end,
  49. {iolist_to_binary(?REST_JSON:encode(Struct)), Req, State};
  50. to_json(#{bindings := #{resource := Module}} = Req, State) ->
  51. io:format("~p ~p~n",[?FUNCTION_NAME,Module]),
  52. M = c(Module),
  53. Struct = [{M, [ M:to_json(Resource) || Resource <- M:get() ] } ],
  54. {iolist_to_binary(?REST_JSON:encode(Struct)), Req, State}.
  55. content_types_accepted(Req, State) ->
  56. {[{<<"application/x-www-form-urlencoded">>, handle_urlencoded_data},
  57. {<<"application/json">>, handle_json_data}], Req, State}.
  58. handle_urlencoded_data(#{bindings := #{resource := Module}} = Req0, State) ->
  59. {ok, Data1, Req} = cowboy_req:read_urlencoded_body(Req0),
  60. io:format("FORM: ~p, Data1: ~p~n",[Module,Data1]),
  61. {handle_data(c(Module), [], Data1, Req), Req, State};
  62. handle_urlencoded_data(#{bindings := #{resource := Module, id := Id}} = Req, State) ->
  63. {ok, Data, Req2} = cowboy_req:read_urlencoded_body(Req),
  64. io:format("FORM: ~p~n",[Data]),
  65. {handle_data(c(Module), Id, Data, Req), Req2, State}.
  66. handle_json_data(#{bindings := #{resource := Module}} = Req, State) ->
  67. {ok, Binary, Req2} = cowboy_req:read_body(Req),
  68. io:format("JSON: ~p~n",[Binary]),
  69. Data = case ?REST_JSON:decode(Binary) of {struct, Struct} -> Struct; S -> S end,
  70. {handle_data(c(Module), [], Data, Req), Req2, State};
  71. handle_json_data(#{bindings := #{resource := Module, id := Id}} = Req, State) ->
  72. {ok, Binary, Req2} = cowboy_req:read_body(Req),
  73. io:format("JSON: ~p~n",[Binary]),
  74. Data = case ?REST_JSON:decode(Binary) of {struct, Struct} -> Struct; S -> S end,
  75. {handle_data(c(Module), Id, Data, Req), Req2, State}.
  76. handle_data(Mod, Id, Data, Req) ->
  77. io:format("handle_data(~p)~n",[{Mod,Id,Data,Req}]),
  78. Valid = case erlang:function_exported(Mod, validate, 2) of
  79. true -> Mod:validate(Id, Data);
  80. false -> default_validate(Mod, Id, Data, Req) end,
  81. io:format("Valid ~p Id ~p~n",[Valid,Id]),
  82. case {Valid, Id} of
  83. {false, _} -> false;
  84. {true, []} -> Mod:post(Data);
  85. {true, <<"undefined">>} -> Mod:post(Data);
  86. {true, _} -> case erlang:function_exported(Mod, put, 2) of
  87. true -> Mod:put(Id, Data);
  88. false -> default_put(Mod, Id, Data, Req) end
  89. end.
  90. default_put(Mod, Id, Data, Req) when is_map(Data) -> default_put(Mod, Id, maps:to_list(Data), Req);
  91. default_put(Mod, Id, Data, Req) ->
  92. NewRes = Mod:from_json(Data, Mod:get(Id)),
  93. NewId = proplists:get_value(id, Mod:to_json(NewRes)),
  94. io:format("Id ~p NewId ~p~n",[Id,NewId]),
  95. case Id =/= NewId of
  96. true when Id =:= [] -> skip;
  97. true -> Mod:delete(Id);
  98. false -> true end,
  99. Mod:post(NewRes).
  100. default_validate(Mod, Id, DataX, Req0) when is_map(DataX) -> default_validate(Mod, Id, maps:to_list(DataX), Req0);
  101. default_validate(Mod, Id, Data, Req0) ->
  102. Allowed = case erlang:function_exported(Mod, keys_allowed, 1) of
  103. true -> Mod:keys_allowed(proplists:get_keys(Data));
  104. false -> true end,
  105. validate_match(Mod, Id, Allowed, proplists:get_value(<<"id">>, Data)).
  106. validate_match(_Mod, [], true, []) -> false;
  107. validate_match( Mod, [], true, NewId) -> not Mod:exists(NewId);
  108. validate_match(_Mod, _Id, true, []) -> true;
  109. validate_match(_Mod, Id, true, Id) -> true;
  110. validate_match( Mod, _Id, true, NewId) -> not Mod:exists(NewId);
  111. validate_match( _, _, _, _) -> false.
  112. delete_resource(#{bindings := #{resource := Module, id := []}} = Req, State) -> {[], Req, State};
  113. delete_resource(#{bindings := #{resource := Module, id := Id}} = Req, State) ->
  114. M = c(Module),
  115. io:format("DELETE: ~p ~p ~p~n",[M,Id,M:delete(Id)]),
  116. {M:delete(Id), Req, State}.
  117. rest_module(Module) when is_binary(Module) -> rest_module(binary_to_list(Module));
  118. rest_module(Module) ->
  119. try M = list_to_existing_atom(Module),
  120. Info = proplists:get_value(attributes, M:module_info()),
  121. true = lists:member(rest, proplists:get_value(behaviour, Info)),
  122. {ok, M}
  123. catch error:Error -> {error, Error} end.