Additional parts of the API, DB layer.
Implemented additional API functions: * dispatch_timeline/4 * dispatch_event_by_id/4 * get_timeline/3 * put_timeline/3 * post_timeline/3 * make_json_404/2, make_json_405/2, make_json_500/1 Implemented ts_timeline:lookup/2 Implemented ts_entry:lookup/2
This commit is contained in:
parent
b690326cf4
commit
309d6915fc
10
doc/api.rst
10
doc/api.rst
@ -87,11 +87,11 @@ DELETE
|
||||
|
||||
Delete a timeline.
|
||||
|
||||
Events: ``/<user-id>/<timeline-id>/events``
|
||||
-------------------------------------------
|
||||
List Events
|
||||
-----------
|
||||
|
||||
``/<user-id>/<timeline-id>/list``
|
||||
---------------------------------
|
||||
|
||||
GET
|
||||
~~~
|
||||
|
||||
PUT
|
||||
~~~
|
||||
|
@ -1,8 +1,13 @@
|
||||
/jdbernard -- information about the user 'jdbernard'
|
||||
/jdbernard/work -- information about the 'work' timeline
|
||||
/jdbernard/work/events -- list events in the timeline
|
||||
/jdbernard/work/list
|
||||
/jdbernard/work/by_id/13
|
||||
/jdbernard/work/by_date/2010-11-14
|
||||
/jdbernard/work/by_date/2010-11-14T12.34.26
|
||||
|
||||
/jdbernard/timelines -- list all timelines for the user
|
||||
/jdbernard/timelines/work -- list information about the 'work' timeline
|
||||
|
||||
/jdbernard/events/work -- list all events for the 'work' timeline
|
||||
/jdbernard/work/since/DATE-REF
|
||||
/jdbernard/work/since/2010
|
||||
/jdbernard/work/since/2010/11
|
||||
/jdbernard/work/since/january
|
||||
/jdbernard/work/since/monday
|
||||
/jdbernard/work/since
|
||||
|
143
src/ts_api.erl
143
src/ts_api.erl
@ -21,7 +21,7 @@ out(YArg) ->
|
||||
|
||||
%% Entry point to the TimeStamper API dispatch system
|
||||
dispatch_request(_YArg, []) ->
|
||||
% no arguments: URL is /ts_api or /ts_api/, shoe API docs
|
||||
% no arguments: URL is /ts_api or /ts_api/, show API docs
|
||||
{page, "/ts_api_doc/index.html"};
|
||||
|
||||
dispatch_request(YArg, [H|T]) ->
|
||||
@ -34,26 +34,122 @@ dispatch_request(YArg, [H|T]) ->
|
||||
dispatch_timeline(YArg, Username, Timeline, Params)
|
||||
end.
|
||||
|
||||
% no events, show timeline pages
|
||||
% no entries, show timeline pages
|
||||
dispatch_timeline(YArg, Username, Timeline, []) ->
|
||||
Req = YArg#arg.arg,
|
||||
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)
|
||||
'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),
|
||||
|
||||
case Param of ->
|
||||
list -> list_entries(YArg, Username, Timeline);
|
||||
by_id -> dispatch_entry_by_id(YArg, Username, Timeline, T);
|
||||
by_date -> dispatch_entry_by_date(YArg, Username, Timeline, T);
|
||||
_Other -> make_json_404(YArg)
|
||||
end.
|
||||
|
||||
dispatch_entry_by_id(YArg, Username, Timeline, []) ->
|
||||
make_json_404(YArg, [{note, "An entry id is expected in the URL."}];
|
||||
|
||||
dispatch_entry_by_id(YArg, Username, Timeline, [H|T]) ->
|
||||
|
||||
EventId = list_to_integer(H), % TODO: guard against bad input
|
||||
get_entry_by_id(Username, Timeline, EventId).
|
||||
|
||||
dispatch_entry_by_date(YArg, Username, Timeline, Params) -> todo.
|
||||
|
||||
% ============================== %
|
||||
% ======== IMPLEMENTATION ====== %
|
||||
% ============================== %
|
||||
|
||||
get_timeline(YArg, Username, TimelineId) ->
|
||||
Timeline = mnesia:dirty_read(ts_timeline, {Username, TimelineId}).
|
||||
case ts_timeline:lookup(Username, TimelineId) of
|
||||
|
||||
no_record -> make_json_404(YArg);
|
||||
|
||||
Timeline ->
|
||||
EJSONTimeline = ts_json:record_to_ejson(Timeline),
|
||||
JSONReturn = json:encode({struct, [
|
||||
{status, "ok"},
|
||||
{timeline, EJSONTimeline}
|
||||
]}),
|
||||
{content, "application/json", JSONReturn}
|
||||
end.
|
||||
|
||||
put_timeline(YArg, Username, TimelineId) ->
|
||||
|
||||
% parse the request body
|
||||
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
||||
|
||||
% parse into a Timeline record
|
||||
EmptyTimeline = #ts_timeline{ref = {Username, TimelineId}},
|
||||
NewRecord = ts_json:ejson_to_record(EmptyTimeline, EJSON),
|
||||
|
||||
% insert into the database
|
||||
case ts_timeline:new(NewRecord) of
|
||||
% record created
|
||||
ok ->
|
||||
|
||||
EJSONRec = ts_json:record_to_ejson(NewRecord)
|
||||
JSONReturn = json:encode({struct, [
|
||||
{status, "ok"},
|
||||
{record, EJSONRec}
|
||||
]}),
|
||||
|
||||
% return the new record
|
||||
[{status, 201}, {content, "application/json", JSONReturn}];
|
||||
|
||||
% will not create, record exists
|
||||
{error, {record_exists, ExistingRecord}} ->
|
||||
|
||||
EJSONRec = ts_json:record_to_ejson(ExistingRecord),
|
||||
JSONReturn = json:encode({struct, [
|
||||
{status, "ignored"},
|
||||
{record, EJSONRec},
|
||||
{see_docs, "/ts_api_doc/timeline#PUT"}
|
||||
]}),
|
||||
|
||||
{content, "application/json", JSONReturn};
|
||||
|
||||
_Error -> make_json_500(YArg)
|
||||
end.
|
||||
|
||||
post_timeline(YArg, Username, TimelineId) ->
|
||||
% parse the POST data
|
||||
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
||||
|
||||
% create the timeline record
|
||||
EmptyTimeline = #ts_timeline{ref = {Username, TimelineId}},
|
||||
NewRecord = ts_json:ejson_to_record(EmptyTimeline, EJSON),
|
||||
|
||||
case ts_timeline:update(NewRecord) of
|
||||
ok ->
|
||||
|
||||
EJSONRec = ts_json:record_to_ejson(NewRecord),
|
||||
JSONReturn = json:encode({struct, [
|
||||
{status, "updated"},
|
||||
{record, EJSONRec}
|
||||
]}),
|
||||
|
||||
{content, "application/json", JSONReturn};
|
||||
|
||||
no_record -> make_json_404(YArg, [{status, "no_record"},
|
||||
{see_docs, "/ts_api_doc/timeline#PSOT"}]);
|
||||
|
||||
_Error -> make_json_500(YArg)
|
||||
end.
|
||||
|
||||
delete_timeline(YArg, Username, TimelineId) -> todo.
|
||||
|
||||
% ============================== %
|
||||
% ======== UTIL METHODS ======== %
|
||||
@ -62,3 +158,36 @@ get_timeline(YArg, Username, TimelineId) ->
|
||||
%% Convert one path element to an atom.
|
||||
path_element_to_atom(PE) ->
|
||||
list_to_atom(re:replace(PE, "\\s", "_", [{return, list}])).
|
||||
|
||||
%% Create a JSON 404 response.
|
||||
make_json_404(YArg) -> make_json_404(YArg, []).
|
||||
make_json_404(YArg, Fields) ->
|
||||
% add default status if not provided
|
||||
F1 = case lists:keyfind(status, 1, Fields) of
|
||||
false -> Fields ++ [{status, not_found}];
|
||||
_Else -> Fields
|
||||
end,
|
||||
|
||||
% add the path they requested
|
||||
F2 = F1 ++ [{path, (YArg#arg.req)#http_request.path}],
|
||||
|
||||
[{status, 404}, {content, "application/json", json:encode({struct, Fields})}].
|
||||
|
||||
make_json_405(YArg) -> make_json_405(YArg, []).
|
||||
make_json_405(YArg, Fields) ->
|
||||
% add default status if not provided
|
||||
F1 = case lists:keyfind(status, 1, Fields) of
|
||||
false -> Fields ++ [{status, method_not_allowed}];
|
||||
_Else -> Fields
|
||||
end,
|
||||
|
||||
% add the path they requested
|
||||
F2 = F1 ++ [{path, (YArg#arg.req)#http_request.path}],
|
||||
|
||||
[{status, 405}, {content, "application/json", json:encode({struct, Fields})}].
|
||||
|
||||
make_json_500(_YArg) ->
|
||||
EJSON = {struct, [
|
||||
{status, "error"},
|
||||
{error, "Internal Server Error"}]},
|
||||
[{status, 500}, {content, "application/json", json:encode(EJSON)}].
|
||||
|
@ -5,7 +5,7 @@
|
||||
}).
|
||||
|
||||
-record(ts_entry, {
|
||||
ref, % {username, timelineid, eventid}
|
||||
ref, % {username, timelineid, entryid}
|
||||
timestamp, % gregorian seconds
|
||||
mark, % String description of entry
|
||||
notes % String with further notes about the entry
|
||||
|
@ -1,5 +1,5 @@
|
||||
-module(ts_entry).
|
||||
-export([create_table/1, new/1, update/1, list/3]).
|
||||
-export([create_table/1, new/1, update/1, lookup/3, list/3]).
|
||||
|
||||
-include("ts_db_records.hrl").
|
||||
-include_lib("stdlib/include/qlc.hrl").
|
||||
@ -28,6 +28,12 @@ update(ER = #ts_entry()) ->
|
||||
[_Record] -> mnesia:dirty_write(ER)
|
||||
end.
|
||||
|
||||
lookup(Username, TimelineId, EntryId) ->
|
||||
case mnesia:dirty_read(ts_entry, {Username, TimelineId, EntryId}) of
|
||||
[] -> no_record;
|
||||
[Entry] -> Entry
|
||||
end.
|
||||
|
||||
list({Username, Timeline}, Start, Length)
|
||||
when is_integer(Start) and is_integer(Length) ->
|
||||
ts_common:list(
|
||||
|
74
src/ts_json.erl
Normal file
74
src/ts_json.erl
Normal file
@ -0,0 +1,74 @@
|
||||
-module(ts_json).
|
||||
-export([encode_record/1, record_to_ejson/1, ejson_to_record/2]).
|
||||
|
||||
-include("ts_db_records.hrl").
|
||||
|
||||
encode_record(Record) -> lists:flatten(json:encode(record_to_ejson(Record))).
|
||||
|
||||
record_to_ejson(Record=#ts_timeline{}) ->
|
||||
% pull out the timeline id
|
||||
{_Username, TimelineId} = Record#ts_timeline.ref,
|
||||
|
||||
% create the EJSON struct
|
||||
{struct, [
|
||||
{id, atom_to_list(TimelineId)},
|
||||
{created, encode_datetime(Record#ts_timeline.created)},
|
||||
{description, Record#ts_timeline.desc}]}.
|
||||
|
||||
record_to_ejson(Record=#ts_entry{}) ->
|
||||
% pull out the entry id
|
||||
{_Username, _TimelineId, EntryId} = Record#ts_entry.ref,
|
||||
|
||||
% convert the timestamp to a date-time
|
||||
DateTime = calendar:gregorian_seconds_to_datetime(Record#ts_entry.timestamp),
|
||||
|
||||
% create the EJSON struct
|
||||
{struct, [
|
||||
{id, EntryId},
|
||||
{timestamp, encode_datetime(DateTime)},
|
||||
{mark, Record#ts_entry.mark},
|
||||
{notes, Record#ts_entry.notes}]}.
|
||||
|
||||
encode_datetime({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
io_lib:format("~2B-~2B-~2BT~2.10.0B:~2.10.0B:~2.10.0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second]).
|
||||
|
||||
ejson_to_record(Empty=#ts_timeline{}, EJSON) ->
|
||||
{struct, Fields} = EJSON,
|
||||
{Username, _} = Empty#ts_timeline.ref,
|
||||
|
||||
#ts_timeline{
|
||||
ref = {Username, element(2, lists:keyfind(id, 1, EJSON))},
|
||||
created = decode_datetime(element(2, lists:keyfind(created, 1, EJSON))),
|
||||
desc = element(2, lists:keyfind(description, 1, EJSON))}.
|
||||
|
||||
ejson_to_record(Empty=#ts_entry{}, EJSON) ->
|
||||
{struct, Fields} = EJSON,
|
||||
{Username, TimelineId, _} = Empty#ts_entry.ref,
|
||||
|
||||
#ts_entry{
|
||||
ref = {Username, TimelineId, element(2, lists:keyfind(id, 1, EJSON))},
|
||||
timestamp = calendar:datetime_to_gregorian_seconds(decode_datetime(
|
||||
element(2, lists:keyfind(timestamp, 1, EJSON)))),
|
||||
mark = element(2, lists:keyfind(mark, 1, EJSON)),
|
||||
notes = element(2, lists:keyfind(notes, 1, EJSON))}.
|
||||
|
||||
decode_datetime(DateTimeString) ->
|
||||
% TODO: catch badmatch and badarg on whole function
|
||||
|
||||
[DateString, TimeString] = re:split(DateTimeString, "[TZ]",
|
||||
[{return, list}, trim]),
|
||||
|
||||
[YearString, MonthString, DayString] =
|
||||
re:split(DateString, "-", [{return, list}]),
|
||||
|
||||
[HourString, MinuteString, SecondString] =
|
||||
re:split(TimeString, ":", [{return, list}]),
|
||||
|
||||
Date = {list_to_integer(YearString), list_to_integer(MonthString),
|
||||
list_to_integer(DayString)}
|
||||
|
||||
Time = {list_to_integer(HourString), list_to_integer(MinuteString),
|
||||
list_to_integer(SecondString)}
|
||||
|
||||
{Date, Time}.
|
@ -1,5 +1,5 @@
|
||||
-module(ts_timeline).
|
||||
-export([create_table/1, new/1, update/1, list/3]).
|
||||
-export([create_table/1, new/1, update/1, lookup/2, list/3]).
|
||||
|
||||
-include("ts_db_records.hrl").
|
||||
-include_lib("stdlib/include/qlc.hrl").
|
||||
@ -25,6 +25,12 @@ update(TR = #ts_timeline{}) ->
|
||||
[_Record] -> mnesia:dirty_write(TR)
|
||||
end.
|
||||
|
||||
lookup(Username, TimlineId) ->
|
||||
case mnesia:dirty_read(ts_timeline, {Username, TimelineId}) of
|
||||
[] -> no_record;
|
||||
[Timeline] -> Timeline
|
||||
end.
|
||||
|
||||
list(Username, Start, Length) ->
|
||||
ts_common:list(
|
||||
qlc:q([T || T <- mnesia:table(ts_timeline),
|
||||
|
Loading…
x
Reference in New Issue
Block a user