Skip to content

Commit

Permalink
[#1] Provide documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Brujo Benavides committed Dec 2, 2015
1 parent e238436 commit 29b670d
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 20 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ CT_OPTS += -cover test/cover.spec -vvv -erl_args -boot start_sasl -config ${CONF

SHELL_OPTS += -name ${PROJECT}@`hostname` -config ${CONFIG} -boot start_sasl -s sync

EDOC_OPTS += todo, report_missing_types

quicktests: app
@$(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)"
$(verbose) mkdir -p $(CURDIR)/logs/
Expand Down
8 changes: 8 additions & 0 deletions doc/overview.edoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@author Brujo Benavides <[email protected]>
@copyright 2015 Erlang Solutions Ltd.
@version 0.0.1
@title Sumo Rest
@doc <strong>Sumo Rest</strong> gives you generic <strong>Cowboy</strong>
handlers to work with <strong>Sumo DB</strong>
@reference Check <a href="https://github.com/inaka/sumo_rest">Github</a>
for more information.
38 changes: 34 additions & 4 deletions src/sr_entities_handler.erl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
%%% @doc Base GET|POST /[entity]s implementation
%%% @doc Base GET|POST /[entities] implementation
-module(sr_entities_handler).

-export([ init/3
Expand All @@ -25,18 +25,28 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Cowboy Callbacks
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% @doc Upgrades to cowboy_rest.
%% Basically, just returns <code>{upgrade, protocol, cowboy_rest}</code>
%% @see cowboy_rest:init/3
-spec init({atom(), atom()}, cowboy_req:req(), options()) ->
{upgrade, protocol, cowboy_rest}.
init(_Transport, _Req, _Opts) ->
{upgrade, protocol, cowboy_rest}.

%% @doc Announces the Req and moves on.
%% If <code>verbose := true</code> in <code>Opts</code> for this handler
%% prints out a line indicating that endpoint that was hit.
%% @see cowboy_rest:rest_init/2
-spec rest_init(cowboy_req:req(), options()) ->
{ok, cowboy_req:req(), state()}.
rest_init(Req, Opts) ->
Req1 = announce_req(Req, Opts),
{ok, Req1, #{opts => Opts}}.

%% @doc Retrieves the list of allowed methods from Trails metadata.
%% Parses the metadata associated with this path and returns the
%% corresponding list of endpoints.
%% @see cowboy_rest:allowed_methods/2
-spec allowed_methods(cowboy_req:req(), state()) ->
{[binary()], cowboy_req:req(), state()}.
allowed_methods(Req, State) ->
Expand All @@ -45,26 +55,37 @@ allowed_methods(Req, State) ->
Methods = [atom_to_method(Method) || Method <- maps:keys(Metadata)],
{Methods, Req, State}.

%% @doc Returns <code>false</code> for POST, <code>true</code> otherwise.
%% @see cowboy_rest:resource_exists/2
-spec resource_exists(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
resource_exists(Req, State) ->
{Method, Req1} = cowboy_req:method(Req),
{Method =/= <<"POST">>, Req1, State}.

%% @doc Always returns "application/json *" with <code>handle_post</code>.
%% @see cowboy_rest:content_types_accepted/2
%% @todo Use swagger's 'consumes' to auto-generate this if possible
%% @see https://github.com/inaka/sumo_rest/issues/7
%% <a href="https://github.com/inaka/sumo_rest/issues/7">Issue</a>
-spec content_types_accepted(cowboy_req:req(), state()) ->
{[{{binary(), binary(), '*'}, atom()}], cowboy_req:req(), state()}.
content_types_accepted(Req, State) ->
{[{{<<"application">>, <<"json">>, '*'}, handle_post}], Req, State}.

%% @doc Always returns "application/json" with <code>handle_get</code>.
%% @see cowboy_rest:content_types_provided/2
%% @todo Use swagger's 'produces' to auto-generate this if possible
%% @see https://github.com/inaka/sumo_rest/issues/7
%% <a href="https://github.com/inaka/sumo_rest/issues/7">Issue</a>
-spec content_types_provided(cowboy_req:req(), state()) ->
{[{binary(), atom()}], cowboy_req:req(), state()}.
content_types_provided(Req, State) ->
{[{<<"application/json">>, handle_get}], Req, State}.

%% @doc Returns the list of all entities.
%% Fetches the entities from <strong>SumoDB</strong> using the
%% <code>model</code> provided in the options.
%% @todo Use query-string as filters.
%% <a href="https://github.com/inaka/sumo_rest/issues/8">Issue</a>
-spec handle_get(cowboy_req:req(), state()) ->
{iodata(), cowboy_req:req(), state()}.
handle_get(Req, State) ->
Expand All @@ -74,6 +95,9 @@ handle_get(Req, State) ->
JSON = sr_json:encode(Reply),
{JSON, Req, State}.

%% @doc Creates a new entity.
%% To parse the body, it uses <code>from_json/2</code> from the
%% <code>model</code> provided in the options.
-spec handle_post(cowboy_req:req(), state()) ->
{{true, binary()} | false | halt, cowboy_req:req(), state()}.
handle_post(Req, State) ->
Expand All @@ -100,6 +124,8 @@ handle_post(Req, State) ->
{false, Req3, State}
end.

%% @doc Persists a new entity.
%% The body must have been parsed beforehand.
-spec handle_post(sumo:user_doc(), cowboy_req:req(), state()) ->
{{true, binary()}, cowboy_req:req(), state()}.
handle_post(Entity, Req1, State) ->
Expand All @@ -122,6 +148,10 @@ handle_post(Entity, Req1, State) ->
Location = iolist_to_binary([Path, $/, Model:uri_path(PersistedEntity)]),
{{true, Location}, Req2, State}.

%% @doc Announces the Req.
%% If <code>verbose := true</code> in <code>Opts</code> for this handler
%% prints out a line indicating that endpoint that was hit.
%% @see cowboy_rest:rest_init/2
-spec announce_req(cowboy_req:req(), options()) -> cowboy_req:req().
announce_req(Req, #{verbose := true}) ->
{Method, Req1} = cowboy_req:method(Req),
Expand Down
11 changes: 11 additions & 0 deletions src/sr_json.erl
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@

-export_type([json/0]).

%% @doc Internal representation to string
-spec encode(json()) -> iodata().
encode(Json) -> jiffy:encode(Json, [uescape]).

%% @doc String to internal representation
-spec decode(iodata()) -> json().
decode(Data) ->
try jiffy:decode(Data, [return_maps])
Expand All @@ -35,22 +37,31 @@ decode(Data) ->
throw(badjson)
end.

%% @doc Format datetimes as binaries using iso8601
-spec encode_date(calendar:datetime()) -> binary().
encode_date(DateTime) -> iso8601:format(DateTime).

%% @doc Parse binaries as datetimes using iso8601
%% @todo remove binary_to_list when is8601 specs are fixed
-spec decode_date(binary()) -> calendar:datetime().
decode_date(DateTime) -> iso8601:parse(binary_to_list(DateTime)).

%% @doc Encode 'undefined' as 'null'.
%% Leave everything else as is.
-spec encode_null(undefined) -> null
; (json()) -> json().
encode_null(undefined) -> null;
encode_null(Json) -> Json.

%% @doc Decode 'null' as 'undefined'.
%% Leave everything else as is.
-spec decode_null(null) -> undefined
; (non_null_json()) -> json().
decode_null(null) -> undefined;
decode_null(Json) -> Json.

%% @doc Format errors as jsons.
%% Given the error Reason, this function returns the json equivalent to
%% <code>{"error": "Reason"}</code>.
-spec error(binary()) -> iodata().
error(Error) -> encode(#{error => Error}).
35 changes: 32 additions & 3 deletions src/sr_single_entity_handler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,23 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Cowboy Callbacks
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% @doc Announces the Req and moves on.
%% It extracts the <code>:id</code> binding from the Req and leaves it in
%% the <code>id</code> key in the state.
%% @see cowboy_rest:rest_init/2
-spec rest_init(cowboy_req:req(), options()) ->
{ok, cowboy_req:req(), state()}.
rest_init(Req, Opts) ->
Req1 = announce_req(Req, Opts),
{Id, Req2} = cowboy_req:binding(id, Req1),
{ok, Req2, #{opts => Opts, id => Id}}.

%% @doc Verifies if there is an entity with the given <code>id</code>.
%% The provided id must be the value for the id field in
%% <strong>SumoDb</strong>. If the entity is found, it's kept in the
%% state.
%% @see cowboy_rest:resource_exists/2
%% @see sumo:find/2
-spec resource_exists(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
resource_exists(Req, State) ->
Expand All @@ -49,22 +58,35 @@ resource_exists(Req, State) ->
Entity -> {true, Req, State#{entity => Entity}}
end.

%% @todo Use swagger's 'consumes' to auto-generate this if possible
%% @see https://github.com/inaka/sumo_rest/issues/7
%% @doc Always returns "application/json *".
%% The function depends on the request method, it can be
%% <ul>
%% <li> <code>handle_put</code> </li>
%% <li> <code>handle_patch</code> </li>
%% </ul>
%% @see cowboy_rest:content_types_accepted/2
%% @todo Use swagger's 'consumes' to auto-generate this if possible.
%% <a href="https://github.com/inaka/sumo_rest/issues/7">Issue</a>
-spec content_types_accepted(cowboy_req:req(), state()) ->
{[{{binary(), binary(), '*'}, atom()}], cowboy_req:req(), state()}.
content_types_accepted(Req, State) ->
{Method, Req1} = cowboy_req:method(Req),
Function = method_function(Method),
{[{{<<"application">>, <<"json">>, '*'}, Function}], Req1, State}.

%% @doc Renders the found entity.
%% @see resource_exists/2
-spec handle_get(cowboy_req:req(), state()) ->
{iodata(), cowboy_req:req(), state()}.
handle_get(Req, State) ->
#{opts := #{model := Model}, entity := Entity} = State,
ResBody = sr_json:encode(Model:to_json(Entity)),
{ResBody, Req, State}.

%% @doc Updates the found entity.
%% To parse the body, it uses <code>update/2</code> from the
%% <code>model</code> provided in the options.
%% @see resource_exists/2
-spec handle_patch(cowboy_req:req(), state()) ->
{{true, binary()} | false | halt, cowboy_req:req(), state()}.
handle_patch(Req, #{entity := Entity} = State) ->
Expand All @@ -81,6 +103,11 @@ handle_patch(Req, #{entity := Entity} = State) ->
{false, Req3, State}
end.

%% @doc Updates the entity if found, otherwise it creates a new one.
%% To parse the body, it uses either <code>update/2</code> or
%% <code>from_json/2</code> (if defined) or <code>from_json/1</code>
%% from the <code>model</code> provided in the options.
%% @see resource_exists/2
-spec handle_put(cowboy_req:req(), state()) ->
{{true, binary()} | false | halt, cowboy_req:req(), state()}.
handle_put(Req, #{entity := Entity} = State) ->
Expand Down Expand Up @@ -110,6 +137,8 @@ handle_put(Req, #{id := Id} = State) ->
{false, Req3, State}
end.

%% @doc Deletes the found entity.
%% @see resource_exists/2
-spec delete_resource(cowboy_req:req(), state()) ->
{boolean() | halt, cowboy_req:req(), state()}.
delete_resource(Req, State) ->
Expand Down
17 changes: 4 additions & 13 deletions src/sumo_rest_doc.erl
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
%%% @doc Implement this behavior on your entities so the handlers can
%%% properly (un)marshall them.
%%% properly [un]marshall them.
-module(sumo_rest_doc).

-type key() :: binary() | atom().
-type object() :: #{key() => json()}.
-type json() :: object()
| [object()]
| binary()
| number()
| boolean()
| null.

-export_type([json/0]).
-type json() :: sr_json:json().

-type entity() :: sumo:user_doc().
-export_type([entity/0]).
Expand All @@ -23,9 +14,9 @@
-callback from_json(json()) -> {ok, entity()} | {error, reason()}.
-callback update(entity(), json()) -> {ok, entity()} | {error, reason()}.
-callback uri_path(entity()) -> iodata().
%% @doc it's only needed if dups should raise 409 conflict
%% it's only needed if dups should raise 409 conflict
-callback id(entity()) -> term().
%% @doc it's only needed if ids are not coming in PUT jsons
%% it's only needed if ids are not coming in PUT jsons
-callback from_json(binary(), json()) -> {ok, entity()} | {error, reason()}.

-optional_callbacks([id/1, from_json/2]).

0 comments on commit 29b670d

Please sign in to comment.