Continuing work on the API.

The id_counter module now includes the record directly in the source.
Fixed many typos and small syntax errors.
Added ts_api:dispatch_user/2 to handle different HTTP methods.
Added method placeholder stubs to ts_api.
Implemented ts_api:list_entries/3.
Added ts_user record and ts_user module.
Implemented ts_entry:list/4, the more generic guts of the other list functions.
Created ts_entry:list_asc/3 and ts_entry:list_desc/3.
Fixed ts_json:encode_datetime/1.
This commit is contained in:
Jonathan Bernard 2011-02-02 16:57:58 -06:00
parent 098cd4cbb9
commit 6fe9184c8e
15 changed files with 263 additions and 64 deletions

BIN
db/test/DECISION_TAB.LOG Normal file

Binary file not shown.

BIN
db/test/LATEST.LOG Normal file

Binary file not shown.

1
db/test/id_counter.DCD Normal file
View File

@ -0,0 +1 @@
cXM

BIN
db/test/schema.DAT Normal file

Binary file not shown.

1
db/test/ts_entry.DCD Normal file
View File

@ -0,0 +1 @@
cXM

BIN
db/test/ts_timeline.DCD Normal file

Binary file not shown.

View File

@ -1,7 +1,7 @@
-module(id_counter). -module(id_counter).
-export([create_table/1, next_counter/1, dirty_next_counter/1]). -export([create_table/1, next_counter/1, dirty_next_counter/1]).
-include("vbs_db_records.hrl"). -record(id_counter, {name, next_value = 0}).
%% Create the table structure for Mnesia %% Create the table structure for Mnesia
create_table(Opts) -> create_table(Opts) ->

View File

@ -1,17 +1,19 @@
-module(ts_api). -module(ts_api).
-compile(export_all). -compile(export_all).
-include("ts_db_records/hrl"). -include("ts_db_records.hrl").
-include("yaws_api.hrl"). -include("yaws_api.hrl").
out(YArg) -> out(YArg) ->
%get the app mod data %get the app mod data
PathString = YArg#arg.apmoddata, PathString = YArg#arg.appmoddata,
% split the path % split the path
PathElements = cast PathString of PathElements = case PathString of
undefined -> []; %handle no end slash: /ts_api undefined -> []; %handle no end slash: /ts_api
_Any -> re:split(PathString, "/", [{return, list}]) _Any -> re:split(PathString, "/", [{return, list}])
end, end,
% process request % process request
dispatch_request(YArg, PathElements). dispatch_request(YArg, PathElements).
@ -28,15 +30,26 @@ dispatch_request(YArg, [H|T]) ->
Username = path_element_to_atom(H), Username = path_element_to_atom(H),
case T of case T of
% no additional parameters, show user info % no additional parameters, show user info
[] -> get_user_info(YArg, Username); [] -> dispatch_user(YArg, Username);
[Param|Params] -> [Param|Params] ->
Timeline = path_element_to_atom(Param) Timeline = path_element_to_atom(Param),
dispatch_timeline(YArg, Username, Timeline, Params) dispatch_timeline(YArg, Username, Timeline, Params)
end. 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 % no entries, show timeline pages
dispatch_timeline(YArg, Username, Timeline, []) -> dispatch_timeline(YArg, Username, Timeline, []) ->
Req = YArg#arg.arg, Req = YArg#arg.req,
HTTPMethod = Req#http_request.method, HTTPMethod = Req#http_request.method,
case HTTPMethod of case HTTPMethod of
@ -51,17 +64,17 @@ 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); list -> list_entries(YArg, Username, Timeline);
by_id -> dispatch_entry_by_id(YArg, Username, Timeline, T); by_id -> dispatch_entry_by_id(YArg, Username, Timeline, T);
by_date -> dispatch_entry_by_date(YArg, Username, Timeline, T); by_date -> dispatch_entry_by_date(YArg, Username, Timeline, T);
_Other -> make_json_404(YArg) _Other -> make_json_404(YArg)
end. end.
dispatch_entry_by_id(YArg, Username, Timeline, []) -> dispatch_entry_by_id(YArg, _Username, _Timeline, []) ->
make_json_404(YArg, [{note, "An entry id is expected in the URL."}]; make_json_404(YArg, [{note, "An entry id is expected in the URL."}]);
dispatch_entry_by_id(YArg, Username, Timeline, [H|T]) -> dispatch_entry_by_id(YArg, Username, Timeline, [H|_T]) ->
EventId = list_to_integer(H), % TODO: guard against bad input EventId = list_to_integer(H), % TODO: guard against bad input
get_entry_by_id(YArg, Username, Timeline, EventId). get_entry_by_id(YArg, Username, Timeline, EventId).
@ -72,6 +85,14 @@ dispatch_entry_by_date(YArg, Username, Timeline, Params) -> todo.
% ======== IMPLEMENTATION ====== % % ======== IMPLEMENTATION ====== %
% ============================== % % ============================== %
get_user(YArg, Username) -> todo.
put_user(YArg, Username) -> todo.
post_user(YArg, Username) -> todo.
delete_user(YArg, Username) -> todo.
get_timeline(YArg, Username, TimelineId) -> get_timeline(YArg, Username, TimelineId) ->
case ts_timeline:lookup(Username, TimelineId) of case ts_timeline:lookup(Username, TimelineId) of
@ -100,7 +121,7 @@ put_timeline(YArg, Username, TimelineId) ->
% record created % record created
ok -> ok ->
EJSONRec = ts_json:record_to_ejson(NewRecord) EJSONRec = ts_json:record_to_ejson(NewRecord),
JSONReturn = json:encode({struct, [ JSONReturn = json:encode({struct, [
{status, "ok"}, {status, "ok"},
{timeline, EJSONRec} {timeline, EJSONRec}
@ -157,12 +178,25 @@ list_entries(YArg, Username, Timeline) ->
% 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) Length = case lists:keyfind(length, 1, PostData) of
{length, LengthVal} -> LengthVal; false -> 50 end,
% read or default the sort order % read or default the sort order
SortOrder = case lists:keyfind(order, 1, PostData) of
{order, "asc"} -> asc;
{order, "desc"} -> desc;
_Other -> asc
end,
Entries = case SortOrder of
asc -> ts_entry:list_asc({Username, Timeline}, Start, Length);
desc -> ts_entry:list_desc({Username, Timeline}, Start, Length)
end
.
get_entry_by_id(YArg, Username, Timeline, EventId) -> get_entry_by_id(YArg, Username, Timeline, EventId) ->
case ts_entry:lookup(Username, Timeline, EventId) of case ts_entry:lookup(Username, Timeline, EventId) of
@ -197,14 +231,14 @@ make_json_404(YArg) -> make_json_404(YArg, []).
make_json_404(YArg, Fields) -> make_json_404(YArg, Fields) ->
% add default status if not provided % add default status if not provided
F1 = case lists:keyfind(status, 1, Fields) of F1 = case lists:keyfind(status, 1, Fields) of
false -> Fields ++ [{status, not_found}]; false -> Fields ++ [{status, "not_found"}];
_Else -> Fields _Else -> Fields
end, end,
% add the path they requested % add the path they requested
F2 = F1 ++ [{path, (YArg#arg.req)#http_request.path}], F2 = F1 ++ [{path, element(2, (YArg#arg.req)#http_request.path)}],
[{status, 404}, {content, "application/json", json:encode({struct, Fields})}]. [{status, 404}, {content, "application/json", json:encode({struct, F2})}].
make_json_405(YArg) -> make_json_405(YArg, []). make_json_405(YArg) -> make_json_405(YArg, []).
make_json_405(YArg, Fields) -> make_json_405(YArg, Fields) ->
@ -217,7 +251,7 @@ make_json_405(YArg, Fields) ->
% add the path they requested % add the path they requested
F2 = F1 ++ [{path, (YArg#arg.req)#http_request.path}], F2 = F1 ++ [{path, (YArg#arg.req)#http_request.path}],
[{status, 405}, {content, "application/json", json:encode({struct, Fields})}]. [{status, 405}, {content, "application/json", json:encode({struct, F2})}].
make_json_500(_YArg) -> make_json_500(_YArg) ->
EJSON = {struct, [ EJSON = {struct, [

View File

@ -1,5 +1,5 @@
-module(ts_common). -module(ts_common).
-export([list/3, compare_dates/2]). -export([list/3, order_datetimes/2]).
-include_lib("stdlib/include/qlc.hrl"). -include_lib("stdlib/include/qlc.hrl").

View File

@ -1,3 +1,12 @@
-record(ts_user, {
username,
pwd,
pwd_salt,
name,
email,
join_date
}).
-record(ts_timeline, { -record(ts_timeline, {
ref, % {username, timelineid} ref, % {username, timelineid}
created,% {{year, month, day}, {hour, minute, second}} created,% {{year, month, day}, {hour, minute, second}}

View File

@ -1,5 +1,5 @@
-module(ts_entry). -module(ts_entry).
-export([create_table/1, new/1, update/1, lookup/3, list/3]). -export([create_table/1, new/1, update/1, lookup/3, list_asc/3, list_desc/3]).
-include("ts_db_records.hrl"). -include("ts_db_records.hrl").
-include_lib("stdlib/include/qlc.hrl"). -include_lib("stdlib/include/qlc.hrl").
@ -7,10 +7,10 @@
create_table(TableOpts) -> create_table(TableOpts) ->
mnesia:create_table(ts_entry, mnesia:create_table(ts_entry,
TableOpts ++ [{attributes, record_info(fields, ts_entry)}, TableOpts ++ [{attributes, record_info(fields, ts_entry)},
{type, ordered_set}, {index, timestamp}]). {type, ordered_set}, {index, [timestamp]}]).
new(ER = #ts_entry()) -> new(ER = #ts_entry{}) ->
{atmoic, NewRow) = mnesia:transaction(fun() -> {atmoic, NewRow} = mnesia:transaction(fun() ->
{Username, TimelineId, _} = ER#ts_entry.ref, {Username, TimelineId, _} = ER#ts_entry.ref,
NextId = id_counter:next_counter(ts_entry_id), NextId = id_counter:next_counter(ts_entry_id),
NewRow = ER#ts_entry{ref = {Username, TimelineId, NextId}}, NewRow = ER#ts_entry{ref = {Username, TimelineId, NextId}},
@ -18,7 +18,7 @@ new(ER = #ts_entry()) ->
NewRow end), NewRow end),
{ok, NewRow}. {ok, NewRow}.
update(ER = #ts_entry()) -> update(ER = #ts_entry{}) ->
% look for existing record % look for existing record
case mnesia:dirty_read(ts_entry, ER#ts_entry.ref) of case mnesia:dirty_read(ts_entry, ER#ts_entry.ref) of
@ -34,30 +34,60 @@ lookup(Username, TimelineId, EntryId) ->
[Entry] -> Entry [Entry] -> Entry
end. end.
list({Username, Timeline}, Start, Length) list({Username, Timeline}, Start, Length, OrderFun)
when is_integer(Start) and is_integer(Length) -> when is_integer(Start) and is_integer(Length) ->
ts_common:list(
qlc:q([E || E <- mnesia:table(ts_entry),
E#ts_entry.ref =:= {Username, Timeline, _}]_,
Start, Length).
list({Username, Timeline}, StartDateTime, EndDateTime) ->
{atomic, Entries} = mnesia:transaction(fun() -> {atomic, Entries} = mnesia:transaction(fun() ->
% match the username and timeline
MatchHead = #ts_entry{ref = {Username, Timeline, '_'}, _='_'},
StartSeconds = calendar:datetime_to_gregorian_seconds(StartDateTime), % select all records that match
EndSeconds = calendar:datetime_to_gregorian_seconds(EndDateTime), mnesia:select(ts_entry, [{MatchHead, [], ['$_']}]
% create the query that will select these records
Q = qlc:q([E || E <- mnesia:table(ts_entry),
E#ts_entry.timestamp >= StartSeconds,
E#ts_entry.timestamp < EndSeconds]),
% sort by timestamp
SortedQ = qlc:sort(Q, {order, fun(A, B) ->
A#ts_entry.timestamp > B#ts_entry.timestamp end}),
% return
qlc:e(SortedQ)
end), end),
Entries.
% sort
SortedEntries = lists:sort(OrderFun, Entries),
% return only the range selected.
% TODO: can we do this without selecting all entries?
lists:sublist(SortedEntries, Start, Length);
list({Username, Timeline}, StartDateTime, EndDateTime, OrderFun) ->
% compute the seconds from datetimes
StartSeconds = calendar:datetime_to_gregorian_seconds(StartDateTime),
EndSeconds = calendar:datetime_to_gregorian_seconds(EndDateTime),
% select all entries from the timeline that are within the time range
{atomic, Entries} = mnesia:transaction(fun() ->
% match the username and timeline id
MatchHead = #ts_entry{ref = {Username, Timeline, '_'}, timestamp='$1', _='_'},
% guards for the time range
StartGuard = {'>=', '$1', StartSeconds},
EndGuard = {'<', '$1', EndSeconds},
mnesia:select(ts_entry, [{MatchHead, [StartGuard, EndGuard], ['$_']}])
end,
% sort
lists:sort(OrderFun, Entries).
list_asc(TimelineRef, Start, Length)
when is_integer(Start) and is_integer(Length) ->
list(TimelineRef, Start, Length, fun timestamp_asc/2);
list_asc(TimelineRef, StartDateTime, EndDateTime) ->
list(TimelineRef, StartDateTime, EndDateTime, fun timestamp_asc/2).
list_desc(TimelineRef, Start, Length)
when is_integer(Start) and is_integer(Length) ->
list(TimelineRef, Start, Length, fun timestamp_desc/2);
list_desc(TimelineRef, StartDateTime, EndDateTime) ->
list(TimelineRef, StartDateTime, EndDateTime, fun timestamp_desc/2).
timestamp_asc(E1, E2) -> E1#ts_entry.timestamp > E2#ts_entry.timestamp.
timestamp_desc(E1, E2) -> E1#ts_entry.timestamp < E2#ts_entry.timestamp.

View File

@ -13,7 +13,7 @@ record_to_ejson(Record=#ts_timeline{}) ->
{struct, [ {struct, [
{id, atom_to_list(TimelineId)}, {id, atom_to_list(TimelineId)},
{created, encode_datetime(Record#ts_timeline.created)}, {created, encode_datetime(Record#ts_timeline.created)},
{description, Record#ts_timeline.desc}]}. {description, Record#ts_timeline.desc}]};
record_to_ejson(Record=#ts_entry{}) -> record_to_ejson(Record=#ts_entry{}) ->
% pull out the entry id % pull out the entry id
@ -30,28 +30,28 @@ record_to_ejson(Record=#ts_entry{}) ->
{notes, Record#ts_entry.notes}]}. {notes, Record#ts_entry.notes}]}.
encode_datetime({{Year, Month, Day}, {Hour, Minute, Second}}) -> encode_datetime({{Year, Month, Day}, {Hour, Minute, Second}}) ->
io_lib:format("~2B-~2B-~2BT~2.10.0B:~2.10.0B:~2.10.0BZ", lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0BZ",
[Year, Month, Day, Hour, Minute, Second]). [Year, Month, Day, Hour, Minute, Second])).
ejson_to_record(Empty=#ts_timeline{}, EJSON) -> ejson_to_record(Empty=#ts_timeline{}, EJSON) ->
{struct, Fields} = EJSON, {struct, Fields} = EJSON,
{Username, _} = Empty#ts_timeline.ref, {Username, _} = Empty#ts_timeline.ref,
#ts_timeline{ #ts_timeline{
ref = {Username, element(2, lists:keyfind(id, 1, EJSON))}, ref = {Username, element(2, lists:keyfind(id, 1, Fields))},
created = decode_datetime(element(2, lists:keyfind(created, 1, EJSON))), created = decode_datetime(element(2, lists:keyfind(created, 1, Fields))),
desc = element(2, lists:keyfind(description, 1, EJSON))}. desc = element(2, lists:keyfind(description, 1, Fields))};
ejson_to_record(Empty=#ts_entry{}, EJSON) -> ejson_to_record(Empty=#ts_entry{}, EJSON) ->
{struct, Fields} = EJSON, {struct, Fields} = EJSON,
{Username, TimelineId, _} = Empty#ts_entry.ref, {Username, TimelineId, _} = Empty#ts_entry.ref,
#ts_entry{ #ts_entry{
ref = {Username, TimelineId, element(2, lists:keyfind(id, 1, EJSON))}, ref = {Username, TimelineId, element(2, lists:keyfind(id, 1, Fields))},
timestamp = calendar:datetime_to_gregorian_seconds(decode_datetime( timestamp = calendar:datetime_to_gregorian_seconds(decode_datetime(
element(2, lists:keyfind(timestamp, 1, EJSON)))), element(2, lists:keyfind(timestamp, 1, Fields)))),
mark = element(2, lists:keyfind(mark, 1, EJSON)), mark = element(2, lists:keyfind(mark, 1, Fields)),
notes = element(2, lists:keyfind(notes, 1, EJSON))}. notes = element(2, lists:keyfind(notes, 1, Fields))}.
decode_datetime(DateTimeString) -> decode_datetime(DateTimeString) ->
% TODO: catch badmatch and badarg on whole function % TODO: catch badmatch and badarg on whole function
@ -66,9 +66,9 @@ decode_datetime(DateTimeString) ->
re:split(TimeString, ":", [{return, list}]), re:split(TimeString, ":", [{return, list}]),
Date = {list_to_integer(YearString), list_to_integer(MonthString), Date = {list_to_integer(YearString), list_to_integer(MonthString),
list_to_integer(DayString)} list_to_integer(DayString)},
Time = {list_to_integer(HourString), list_to_integer(MinuteString), Time = {list_to_integer(HourString), list_to_integer(MinuteString),
list_to_integer(SecondString)} list_to_integer(SecondString)},
{Date, Time}. {Date, Time}.

View File

@ -25,15 +25,15 @@ update(TR = #ts_timeline{}) ->
[_Record] -> mnesia:dirty_write(TR) [_Record] -> mnesia:dirty_write(TR)
end. end.
lookup(Username, TimlineId) -> lookup(Username, TimelineId) ->
case mnesia:dirty_read(ts_timeline, {Username, TimelineId}) of case mnesia:dirty_read(ts_timeline, {Username, TimelineId}) of
[] -> no_record; [] -> no_record;
[Timeline] -> Timeline [Timeline] -> Timeline
end. end.
list(Username, Start, Length) -> list(Username, Start, Length) ->
ts_common:list( {atomic, Timelines} = mnesia:transaction(fun() ->
qlc:q([T || T <- mnesia:table(ts_timeline), MatchHead = #ts_timeline{ref = {Username, '_'}, _='_'},
T#ts_timeline.ref =:= {Username, _}]), mnesia:select(ts_timeline, [{MatchHead, [], ['$_']}])
Start, Length). end),
lists:sublist(Timelines, Start, Length).

12
src/ts_user.erl Normal file
View File

@ -0,0 +1,12 @@
-module(ts_user).
-export([]).
-include("ts_db_records.hrl").
-include_lib("stdlib/include/qlc.hrl").
create_table(TableOpts) ->
mnesia:create_table(ts_user,
TableOpts ++ [{attributes, record_info(fields, ts_user)},
{type, ordered_set}]).
%new(TR = #ts_user

112
src/yaws_api.hrl Normal file
View File

@ -0,0 +1,112 @@
%%%----------------------------------------------------------------------
%%% File : yaws_api.hrl
%%% Author : Claes Wikstrom <klacke@hyber.org>
%%% Purpose :
%%% Created : 24 Jan 2002 by Claes Wikstrom <klacke@hyber.org>
%%%----------------------------------------------------------------------
-author('klacke@hyber.org').
-record(arg, {
clisock, %% the socket leading to the peer client
client_ip_port, %% {ClientIp, ClientPort} tuple
headers, %% headers
req, %% request
clidata, %% The client data (as a binary in POST requests)
server_path, %% The normalized server path
%% (pre-querystring part of URI)
querydata, %% For URIs of the form ...?querydata
%% equiv of cgi QUERY_STRING
appmoddata, %% (deprecated - use pathinfo instead) the remainder
%% of the path leading up to the query
docroot, %% Physical base location of data for this request
docroot_mount, %% virtual directory e.g /myapp/ that the docroot
%% refers to.
fullpath, %% full deep path to yaws file
cont, %% Continuation for chunked multipart uploads
state, %% State for use by users of the out/1 callback
pid, %% pid of the yaws worker process
opaque, %% useful to pass static data
appmod_prepath, %% (deprecated - use prepath instead) path in front
%%of: <appmod><appmoddata>
prepath, %% Path prior to 'dynamic' segment of URI.
%% ie http://some.host/<prepath>/<script-point>/d/e
%% where <script-point> is an appmod mount point,
%% or .yaws,.php,.cgi,.fcgi etc script file.
pathinfo %% Set to '/d/e' when calling c.yaws for the request
%% http://some.host/a/b/c.yaws/d/e
%% equiv of cgi PATH_INFO
}).
-record(http_request, {method,
path,
version}).
-record(http_response, {version,
status,
phrase}).
-record(headers, {
connection,
accept,
host,
if_modified_since,
if_match,
if_none_match,
if_range,
if_unmodified_since,
range,
referer,
user_agent,
accept_ranges,
cookie = [],
keep_alive,
location,
content_length,
content_type,
content_encoding,
authorization,
transfer_encoding,
other = [] %% misc other headers
}).
-record(url,
{scheme, %% undefined means not set
host, %% undefined means not set
port, %% undefined means not set
path = [],
querypart = []}).
-record(setcookie,{
key,
value,
quoted,
comment,
comment_url,
discard,
domain,
max_age,
expires,
path,
port,
secure,
version}).
-record(redir_self, {
host, %% string() - our own host
scheme, %% http | https
scheme_str, %% "https://" | "http://"
port, %% integer() - our own port
port_str %% "" | ":<int>" - the optional port part
%% to append to the url
}).