Redesigned API URL structure. Updated ts_api to implement this.
Implemented ts_api:list_timelines/2. Adjusted ts_timeline:list/3 to be 0-indexed. Changed ts_user password hash to use a random salt + SHA1 Added some skeleton testing code.
This commit is contained in:
parent
1575e25898
commit
6e2e0d5f00
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
db/test/ts_user.DCD
Normal file
BIN
db/test/ts_user.DCD
Normal file
Binary file not shown.
187
src/ts_api.erl
187
src/ts_api.erl
@ -22,64 +22,90 @@ out(YArg) ->
|
|||||||
% ================================== %
|
% ================================== %
|
||||||
|
|
||||||
%% Entry point to the TimeStamper API dispatch system
|
%% Entry point to the TimeStamper API dispatch system
|
||||||
dispatch_request(_YArg, []) ->
|
dispatch_request(YArg, []) -> make_json_404(YArg, [{see_docs, "/ts_api_doc"}]);
|
||||||
% no arguments: URL is /ts_api or /ts_api/, show API docs
|
|
||||||
{page, "/ts_api_doc/index.html"};
|
|
||||||
|
|
||||||
dispatch_request(YArg, [H|T]) ->
|
dispatch_request(YArg, [H|T]) ->
|
||||||
Username = path_element_to_atom(H),
|
|
||||||
case T of
|
|
||||||
% no additional parameters, show user info
|
|
||||||
[] -> dispatch_user(YArg, Username);
|
|
||||||
[Param|Params] ->
|
|
||||||
Timeline = path_element_to_atom(Param),
|
|
||||||
dispatch_timeline(YArg, Username, Timeline, Params)
|
|
||||||
end.
|
|
||||||
|
|
||||||
dispatch_user(YArg, Username) ->
|
|
||||||
Req = YArg#arg.req,
|
|
||||||
HTTPMethod = Req#http_request.method,
|
|
||||||
|
|
||||||
case HTTPMethod of
|
|
||||||
'GET' -> get_user(YArg, Username);
|
|
||||||
'PUT' -> put_user(YArg, Username);
|
|
||||||
'POST' -> put_user(YArg, Username);
|
|
||||||
'DELETE' -> delete_user(YArg, Username)
|
|
||||||
end.
|
|
||||||
|
|
||||||
% no entries, show timeline pages
|
|
||||||
dispatch_timeline(YArg, Username, Timeline, []) ->
|
|
||||||
Req = YArg#arg.req,
|
|
||||||
HTTPMethod = Req#http_request.method,
|
|
||||||
|
|
||||||
case HTTPMethod of
|
|
||||||
'GET' -> get_timeline(YArg, Username, Timeline);
|
|
||||||
'PUT' -> put_timeline(YArg, Username, Timeline);
|
|
||||||
'POST' -> post_timeline(YArg, Username, Timeline);
|
|
||||||
'DELETE' -> delete_timeline(YArg, Username, Timeline);
|
|
||||||
_Other -> make_json_405(YArg)
|
|
||||||
end;
|
|
||||||
|
|
||||||
dispatch_timeline(YArg, Username, Timeline, [H|T]) ->
|
|
||||||
|
|
||||||
Param = path_element_to_atom(H),
|
Param = path_element_to_atom(H),
|
||||||
|
|
||||||
case Param of
|
case Param of
|
||||||
list -> list_entries(YArg, Username, Timeline);
|
users -> dispatch_user(YArg, T);
|
||||||
by_id -> dispatch_entry_by_id(YArg, Username, Timeline, T);
|
timelines -> dispatch_timeline(YArg, T);
|
||||||
by_date -> dispatch_entry_by_date(YArg, Username, Timeline, T);
|
entries -> dispatch_entry(YArg, T);
|
||||||
_Other -> make_json_404(YArg)
|
_Other -> make_json_404(YArg, [{see_docs, "/ts_api_doc/"}])
|
||||||
end.
|
end.
|
||||||
|
|
||||||
dispatch_entry_by_id(YArg, _Username, _Timeline, []) ->
|
dispatch_user(YArg, []) ->
|
||||||
make_json_404(YArg, [{note, "An entry id is expected in the URL."}]);
|
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||||
|
|
||||||
dispatch_entry_by_id(YArg, Username, Timeline, [H|_T]) ->
|
case HTTPMethod of
|
||||||
|
'PUT' -> put_user(YArg);
|
||||||
|
_Other -> make_json_404(YArg, [{see_docs, "/ts_api_doc/"}])
|
||||||
|
end;
|
||||||
|
|
||||||
EventId = list_to_integer(H), % TODO: guard against bad input
|
dispatch_user(YArg, [H|T]) ->
|
||||||
get_entry_by_id(YArg, Username, Timeline, EventId).
|
Username = path_element_to_atom(H),
|
||||||
|
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||||
|
|
||||||
dispatch_entry_by_date(YArg, Username, Timeline, Params) -> todo.
|
case {HTTPMethod, T} of
|
||||||
|
{'GET', []} -> get_user(YArg, Username);
|
||||||
|
{'POST', []} -> post_user(YArg, Username);
|
||||||
|
{'DELETE', []} -> delete_user(YArg, Username);
|
||||||
|
{_Other, []} -> make_json_405(YArg, [{see_docs, "/ts_api_doc/users"}]);
|
||||||
|
_Other -> make_json_404(YArg, [{see_docs, "/ts_api_doc/users"}])
|
||||||
|
end.
|
||||||
|
|
||||||
|
% just username, list timelines or create a new one
|
||||||
|
dispatch_timeline(YArg, [UrlUsername]) ->
|
||||||
|
Username = path_element_to_atom(UrlUsername),
|
||||||
|
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||||
|
|
||||||
|
case HTTPMethod of
|
||||||
|
'GET' -> list_timelines(YArg, Username);
|
||||||
|
'PUT' -> put_timeline(YArg, Username);
|
||||||
|
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines"}])
|
||||||
|
end;
|
||||||
|
|
||||||
|
dispatch_timeline(YArg, [UrlUsername, UrlTimelineId]) ->
|
||||||
|
Username = path_element_to_atom(UrlUsername),
|
||||||
|
TimelineId = path_element_to_atom(UrlTimelineId),
|
||||||
|
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||||
|
|
||||||
|
case HTTPMethod of
|
||||||
|
'GET' -> get_timeline(YArg, Username, TimelineId);
|
||||||
|
'POST' -> post_timeline(YArg, Username, TimelineId);
|
||||||
|
'DELETE' -> delete_timeline(YArg, Username, TimelineId);
|
||||||
|
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines"}])
|
||||||
|
end;
|
||||||
|
|
||||||
|
dispatch_timeline(YArg, _Other) ->
|
||||||
|
make_json_404(YArg, [{see_docs, "/ts_api_doc/timelines"}]).
|
||||||
|
|
||||||
|
dispatch_entry(YArg, [UrlUsername, UrlTimelineId]) ->
|
||||||
|
Username = path_element_to_atom(UrlUsername),
|
||||||
|
TimelineId = path_element_to_atom(UrlTimelineId),
|
||||||
|
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||||
|
|
||||||
|
case HTTPMethod of
|
||||||
|
'GET' -> list_entries(YArg, Username, TimelineId);
|
||||||
|
'PUT' -> put_entry(YArg, Username, TimelineId);
|
||||||
|
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/entries"}])
|
||||||
|
end;
|
||||||
|
|
||||||
|
dispatch_entry(YArg, [UrlUsername, UrlTimelineId, UrlEntryId]) ->
|
||||||
|
Username = path_element_to_atom(UrlUsername),
|
||||||
|
TimelineId = path_element_to_atom(UrlTimelineId),
|
||||||
|
EntryId = list_to_integer(UrlEntryId), % TODO: catch non-numbers
|
||||||
|
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||||
|
|
||||||
|
case HTTPMethod of
|
||||||
|
'GET' -> get_entry(YArg, Username, TimelineId, EntryId);
|
||||||
|
'POST' -> post_entry(YArg, Username, TimelineId, EntryId);
|
||||||
|
'DELETE' -> delete_entry(YArg, Username, TimelineId, EntryId);
|
||||||
|
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/entries"}])
|
||||||
|
end;
|
||||||
|
|
||||||
|
dispatch_entry(YArg, _Other) ->
|
||||||
|
make_json_404(YArg, [{see_docs, "/ts_api_doc/entries"}]).
|
||||||
|
|
||||||
% ============================== %
|
% ============================== %
|
||||||
% ======== IMPLEMENTATION ====== %
|
% ======== IMPLEMENTATION ====== %
|
||||||
@ -91,7 +117,7 @@ get_user(YArg, Username) ->
|
|||||||
User -> make_json_200(YArg, User)
|
User -> make_json_200(YArg, User)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
put_user(YArg, Username) ->
|
put_user(YArg) ->
|
||||||
% parse the request body
|
% parse the request body
|
||||||
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
||||||
|
|
||||||
@ -125,29 +151,57 @@ post_user(YArg, Username) ->
|
|||||||
ok -> make_json_200(YArg, NewRecord);
|
ok -> make_json_200(YArg, NewRecord);
|
||||||
|
|
||||||
no_record -> make_json_404(YArg,
|
no_record -> make_json_404(YArg,
|
||||||
[{status, "no_record"}, {see_docs, "/ts_api_doc/user#POST"}]);
|
[{status, "no_such_user"}, {see_docs, "/ts_api_doc/user#POST"}]);
|
||||||
|
|
||||||
_Error -> make_json_500(YArg)
|
_Error -> make_json_500(YArg)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
delete_user(YArg, Username) -> todo.
|
delete_user(YArg, Username) -> todo.
|
||||||
|
|
||||||
|
list_timelines(YArg, Username) ->
|
||||||
|
% pull out the POST data
|
||||||
|
PostData = yaws_api:parse_post(YArg),
|
||||||
|
|
||||||
|
% read or default the Start
|
||||||
|
Start = case lists:keyfind(start, 1, PostData) of
|
||||||
|
{start, StartVal} -> StartVal; false -> 0 end,
|
||||||
|
|
||||||
|
% read or default the Length
|
||||||
|
Length = case lists:keyfind(length, 1, PostData) of
|
||||||
|
{length, LengthVal} -> erlang:min(LengthVal, 50);
|
||||||
|
false -> 50
|
||||||
|
end,
|
||||||
|
|
||||||
|
% list the timelines from the database
|
||||||
|
Timelines = ts_timeline:list(Username, Start, Length),
|
||||||
|
|
||||||
|
% convert them all to their EJSON form
|
||||||
|
EJSONTimelines = {array, lists:map(fun ts_json:record_to_ejson/1, Timelines)},
|
||||||
|
|
||||||
|
% create resposne
|
||||||
|
JSONResponse = json:encode({struct, [
|
||||||
|
{status, "ok"},
|
||||||
|
{timelines, EJSONTimelines}]}),
|
||||||
|
|
||||||
|
% return response
|
||||||
|
{content, "application/json", JSONReponse}.
|
||||||
|
|
||||||
get_timeline(YArg, Username, TimelineId) ->
|
get_timeline(YArg, Username, TimelineId) ->
|
||||||
|
% look for timeline
|
||||||
case ts_timeline:lookup(Username, TimelineId) of
|
case ts_timeline:lookup(Username, TimelineId) of
|
||||||
|
% no such timeline, return 404
|
||||||
no_record -> make_json_404(YArg);
|
no_record -> make_json_404(YArg);
|
||||||
|
% return the timeline data
|
||||||
Timeline -> make_json_200(YArg, Timeline)
|
Timeline -> make_json_200(YArg, Timeline)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
put_timeline(YArg, Username, TimelineId) ->
|
put_timeline(YArg, Username) ->
|
||||||
|
|
||||||
% parse the request body
|
% parse the request body
|
||||||
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
||||||
|
|
||||||
% parse into a Timeline record
|
% parse into a Timeline record
|
||||||
EmptyTimeline = #ts_timeline{ref = {Username, TimelineId}},
|
NewRecord = ts_json:ejson_to_record(#ts_timeline{}, EJSON),
|
||||||
NewRecord = ts_json:ejson_to_record(EmptyTimeline, EJSON),
|
|
||||||
|
|
||||||
% insert into the database
|
% insert into the database
|
||||||
case ts_timeline:new(NewRecord) of
|
case ts_timeline:new(NewRecord) of
|
||||||
@ -174,8 +228,7 @@ post_timeline(YArg, Username, TimelineId) ->
|
|||||||
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
||||||
|
|
||||||
% create the timeline record
|
% create the timeline record
|
||||||
EmptyTimeline = #ts_timeline{ref = {Username, TimelineId}},
|
NewRecord = ts_json:ejson_to_record(#ts_timeline{}, EJSON),
|
||||||
NewRecord = ts_json:ejson_to_record(EmptyTimeline, EJSON),
|
|
||||||
|
|
||||||
case ts_timeline:update(NewRecord) of
|
case ts_timeline:update(NewRecord) of
|
||||||
ok -> make_json_200(YArg, NewRecord);
|
ok -> make_json_200(YArg, NewRecord);
|
||||||
@ -192,13 +245,19 @@ list_entries(YArg, Username, Timeline) ->
|
|||||||
% pull out the POST data
|
% pull out the POST data
|
||||||
PostData = yaws_api:parse_post(YArg),
|
PostData = yaws_api:parse_post(YArg),
|
||||||
|
|
||||||
|
% first determine if we are listing by date
|
||||||
|
case lists:keyfind(byDate, 1, PostData) of
|
||||||
|
{byDate, "true"} ->
|
||||||
|
_Other
|
||||||
% read or default the Start
|
% read or default the Start
|
||||||
Start = case lists:keyfind(start, 1, PostData) of
|
Start = case lists:keyfind(start, 1, PostData) of
|
||||||
{start, StartVal} -> StartVal; false -> 0 end,
|
{start, StartVal} -> StartVal; false -> 0 end,
|
||||||
|
|
||||||
% read or default the Length
|
% read or default the Length
|
||||||
Length = case lists:keyfind(length, 1, PostData) of
|
Length = case lists:keyfind(length, 1, PostData) of
|
||||||
{length, LengthVal} -> LengthVal; false -> 50 end,
|
{length, LengthVal} -> erlang:min(LengthVal, 500);
|
||||||
|
false -> 50
|
||||||
|
end,
|
||||||
|
|
||||||
% read or default the sort order
|
% read or default the sort order
|
||||||
SortOrder = case lists:keyfind(order, 1, PostData) of
|
SortOrder = case lists:keyfind(order, 1, PostData) of
|
||||||
@ -212,15 +271,13 @@ list_entries(YArg, Username, Timeline) ->
|
|||||||
desc -> ts_entry:list_desc({Username, Timeline}, Start, Length)
|
desc -> ts_entry:list_desc({Username, Timeline}, Start, Length)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
get_entry_by_id(YArg, Username, Timeline, EventId) ->
|
get_entry(YArg, Username, TimelineId, EntryId) -> todo.
|
||||||
case ts_entry:lookup(Username, Timeline, EventId) of
|
|
||||||
|
|
||||||
% no such record
|
put_entry(YArg, Username, TimelineId) -> todo.
|
||||||
no_record -> make_json_404(YArg);
|
|
||||||
|
|
||||||
% record found
|
post_entry(YArg, Username, TimelineId, EntryId) -> todo.
|
||||||
Entry -> make_json_200(YArg, Entry)
|
|
||||||
end.
|
delete_entry(YArg, Username, TimelineId, EntryId) -> todo.
|
||||||
|
|
||||||
% ============================== %
|
% ============================== %
|
||||||
% ======== UTIL METHODS ======== %
|
% ======== UTIL METHODS ======== %
|
||||||
|
@ -59,7 +59,6 @@ ejson_to_record(_Empty=#ts_user{}, EJSON) ->
|
|||||||
element(2, lists:keyfind(join_date, 1, Fields)))};
|
element(2, lists:keyfind(join_date, 1, Fields)))};
|
||||||
|
|
||||||
ejson_to_record(_Empty=#ts_timeline{}, EJSON) ->
|
ejson_to_record(_Empty=#ts_timeline{}, EJSON) ->
|
||||||
% The JSON records do not have username information
|
|
||||||
{struct, Fields} = EJSON,
|
{struct, Fields} = EJSON,
|
||||||
Username = element(2, lists:keyfind(username, 1, Fields)),
|
Username = element(2, lists:keyfind(username, 1, Fields)),
|
||||||
TimelineId = element(2, lists:keyfind(timeline_id, 1, Fields)),
|
TimelineId = element(2, lists:keyfind(timeline_id, 1, Fields)),
|
||||||
|
@ -24,4 +24,4 @@ list(Username, Start, Length) ->
|
|||||||
MatchHead = #ts_timeline{ref = {Username, '_'}, _='_'},
|
MatchHead = #ts_timeline{ref = {Username, '_'}, _='_'},
|
||||||
mnesia:select(ts_timeline, [{MatchHead, [], ['$_']}])
|
mnesia:select(ts_timeline, [{MatchHead, [], ['$_']}])
|
||||||
end),
|
end),
|
||||||
lists:sublist(Timelines, Start, Length).
|
lists:sublist(Timelines, Start + 1, Length).
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
-module(ts_user).
|
-module(ts_user).
|
||||||
-export([create_table/1, new/1, update/1, lookup/1, list/2]).
|
%-export([create_table/1, new/1, update/1, lookup/1, list/2]).
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
-include("ts_db_records.hrl").
|
-include("ts_db_records.hrl").
|
||||||
-include_lib("stdlib/include/qlc.hrl").
|
-include_lib("stdlib/include/qlc.hrl").
|
||||||
@ -30,17 +31,13 @@ lookup(Username) ->
|
|||||||
list(Start, Length) -> ts_common:list(ts_user, Start, Length).
|
list(Start, Length) -> ts_common:list(ts_user, Start, Length).
|
||||||
|
|
||||||
hash_input_record(User=#ts_user{}) ->
|
hash_input_record(User=#ts_user{}) ->
|
||||||
% generate the password salt
|
|
||||||
Salt = generate_salt(),
|
|
||||||
% hash the password
|
|
||||||
HashedPwd = hash_pwd(User#ts_user.username, Salt),
|
|
||||||
% create a new User record
|
% create a new User record
|
||||||
|
{HashedPwd, Salt} = hash_pwd(User#ts_user.pwd),
|
||||||
User#ts_user{pwd = HashedPwd, pwd_salt = Salt}.
|
User#ts_user{pwd = HashedPwd, pwd_salt = Salt}.
|
||||||
|
|
||||||
generate_salt() ->
|
generate_salt() -> crypto:rand_bytes(36).
|
||||||
"This is a worthless salt value only suitable for testing.".
|
|
||||||
|
|
||||||
hash_pwd(Password, Salt) -> do_hash(Password ++ Salt, []).
|
hash_pwd(Password) ->
|
||||||
|
Salt = generate_salt(),
|
||||||
do_hash([], Hashed) -> Hashed;
|
Hashed = crypto:sha(Password ++ Salt),
|
||||||
do_hash([Char|Pwd], Hashed) -> do_hash(Pwd, [Char + 13 | Hashed]).
|
{Hashed, Salt}.
|
||||||
|
7
test/ts_test_common.erl
Normal file
7
test/ts_test_common.erl
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-module(ts_test_common).
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("../src/ts_db_records.hrl").
|
||||||
|
|
||||||
|
|
26
test/ts_user_tests.erl
Normal file
26
test/ts_user_tests.erl
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
-module(ts_user_tests).
|
||||||
|
%-import(ts_test_common, [assertRecordsEqual/2]).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("../src/ts_db_records.hrl").
|
||||||
|
|
||||||
|
setup() -> ts_test_env:setup().
|
||||||
|
|
||||||
|
cleanup(_Status) -> ts_api_env:cleanup(ok).
|
||||||
|
|
||||||
|
% ======== TEST DATA ======== %
|
||||||
|
|
||||||
|
joe_user() -> #ts_user{username=joeuser, name="Joe User", pwd="ohmy",
|
||||||
|
email="JoeUser@users.org",
|
||||||
|
join_date=calendar:now_to_datetime(erlang:now())}.
|
||||||
|
|
||||||
|
% ======== TESTS ======== %
|
||||||
|
|
||||||
|
new_delete_test_() ->
|
||||||
|
% test data
|
||||||
|
JoeUser = joe_user(),
|
||||||
|
|
||||||
|
{setup, fun setup/0, fun cleanup/1, {inorder, [
|
||||||
|
?_test(new(JoeUser)),
|
||||||
|
?_test(lookup(JoeUser#ts_user.username, JoeUser)),
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user