Permalink
Browse files

JSON-RPC 2.0 support

Convert existing JSON-RPC support to version 2.0, supporting all
features mentioned here:

http://groups.google.com/group/json-rpc/web/json-rpc-2-0

Add all the examples from that webpage as new tests.

Modify documentation to note that version 2.0 of JSON-RPC is now
supported.
  • Loading branch information...
1 parent 96534b2 commit 15454bcd87690f22059a0a86abc26ea7d10d4933 @vinoski vinoski committed May 2, 2011
Showing with 564 additions and 233 deletions.
  1. +14 −73 src/jsonrpc.erl
  2. +15 −0 src/yaws_jsonrpc.erl
  3. +176 −60 src/yaws_rpc.erl
  4. +4 −2 test/Makefile
  5. +15 −7 test/conf/stdconf.conf
  6. +1 −1 test/t2/Makefile
  7. +221 −0 test/t2/app_test.erl
  8. +28 −0 test/t2/jsontest.erl
  9. +90 −90 www/json_intro.yaws
View
87 src/jsonrpc.erl
@@ -28,85 +28,26 @@
-module(jsonrpc).
-author("Gaspar Chilingarov <nm@web.am>, Gurgen Tumanyan <barbarian@armkb.com>").
--vsn("2").
--export([call/3]).
+-vsn("3").
-export([s/2]). % extract element from proplist
-%%%
-%%% call function calls json-rpc method on remote host
-%%%
-%%% URL - remote server url (may use https)
-%%% Options - option list to be passed to http:request
-%% (ssl options ot timeout, for example)
-%%% Payload -> {call, MethodName, Args} tuple
-%%% MethodName -> atom
-%%% Args -> list
-%%%
-call(URL, Options, Payload) ->
- try
- {ok, CallPayloadDeep} = encode_call_payload(Payload),
- CallPayload = lists:flatten(CallPayloadDeep),
- {ok, Response} = httpc:request(post,
- {URL,[{"Content-length",length(CallPayload)}],
- "application/x-www-form-urlencoded",CallPayload},
- Options, []),
-
- RespBody= if (size(Response) == 2) or (size(Response) == 3) ->
- element(size(Response), Response)
- end,
- decode_call_payload(RespBody)
- catch
- error:Err->
- error_logger:error_report([{'json_rpc:call', error},
- {error, Err},
- {stack, erlang:get_stacktrace()}]),
- {error,Err}
- end.
-
-%%%
-%%% json-rpc.org defines such structure for making call
-%%%
-%%% {"method":"methodname", "params": object, "id": integer}
-encode_call_payload({call, Method, Args}) when is_atom(Method) and
- is_list(Args) ->
- ID = element(3, erlang:now()), % id makes sense when there are many
- % requests in same
- % communication channel and
- %replies can come in random
- %order here it can be changed
- %to something less expensive
- Struct = json2:encode({struct, [{method, atom_to_list(Method)},
- {params, {array, Args}},
- {id, ID}]}),
- {ok, Struct}.
-
-%%%
-%%% decode response structure
-%%%
-%%% {"id":requestID,"result":object,"error":error_description}
-decode_call_payload(JSonStr) ->
- {ok, JSON} = json2:decode_string(JSonStr),
- Result = s(JSON, result),
- Error = s(JSON, error),
-% ID = s(JSON, id), % ignored for now
- if
- (Error =/= undefined) ->
- {error, Error};
- true ->
- {ok,{response,[Result]}} % make it compliant with xmlrpc response
- end.
-
%%% lookup element in proplist
-%%% XXX: are there ready implementation in erlang std library?
-s ({struct, List}, ElemName) ->
+s({struct, List}, ElemName) ->
s(List, ElemName);
-
s(List, ElemName) when is_list(List) ->
case lists:keysearch(ElemName,1,List) of
- {value,{ElemName,Val}} ->
- Val;
- _ ->
- undefined
+ {value,{ElemName,Val}} ->
+ Val;
+ AtomName when is_atom(ElemName) ->
+ ElemList = atom_to_list(ElemName),
+ case lists:keysearch(ElemList,1,List) of
+ {value,{ElemList,Val}} ->
+ Val;
+ _ ->
+ undefined
+ end;
+ _ ->
+ undefined
end.
View
15 src/yaws_jsonrpc.erl
@@ -1,3 +1,18 @@
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED
+%%% WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED
+%%%
+%%% Use module yaws_rpc.erl instead
+%%%
+%%% This module is deprecated.
+%%%
+%%% Do not report problems with this module, as they will not be fixed. You
+%%% should instead convert your code to use the yaws_rpc module.
+%%%
+%%% WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED
+%%% WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Copyright (C) 2003 Joakim Grebenö <jocke@gleipnir.com>.
%% All rights reserved.
%%
View
236 src/yaws_rpc.erl
@@ -38,6 +38,7 @@
-module(yaws_rpc).
-author("Gaspar Chilingarov <nm@web.am>, Gurgen Tumanyan <barbarian@armkb.com>").
-modified_by("Yariv Sadan <yarivvv@gmail.com>").
+-modified_by("Steve Vinoski <vinoski@ieee.org>").
-export([handler/2]).
-export([handler_session/2, handler_session/3]).
@@ -102,35 +103,96 @@ parse_request(Args) ->
{'POST', {1,1}} ->
?Debug("HTTP Version 1.1~n", []),
ok;
- {'POST', _HTTPVersion} -> {status, 505};
- {_Method, {1,1}} -> {status, 501};
- _ -> {status, 400}
+ {'POST', _HTTPVersion} ->
+ {status, 505};
+ {_Method, {1,1}} ->
+ {status, 501};
+ _ ->
+ {status, 400}
end.
-handle_payload(Args, Handler, Type) -> % {{{
+handle_payload(Args, Handler, Type) ->
RpcType = recognize_rpc_type(Args),
%% haXe parameters are URL encoded
+ PL = binary_to_list(Args#arg.clidata),
{Payload,DecodedStr} =
case RpcType of
T when T==haxe; T==json ->
- PL = binary_to_list(Args#arg.clidata),
- ?Debug("rpc plaintext call ~p ~n", [PL]),
+ ?Debug("rpc ~p call ~p~n", [T, PL]),
{PL, yaws_api:url_decode(PL)};
_ ->
- PL = binary_to_list(Args#arg.clidata),
- ?Debug("rpc plaintext call ~p ~n", [PL]),
+ ?Debug("rpc plaintext call ~p~n", [PL]),
{PL, PL}
end,
case decode_handler_payload(RpcType, DecodedStr) of
+ Batch when RpcType == json, is_list(Batch) ->
+ BatchRes =
+ lists:foldl(fun(Req, Acc) ->
+ Result = check_decoded_payload(Args, Handler,
+ Req, Payload,
+ Type, json),
+ case Result of
+ empty ->
+ Acc;
+ {result, _Code, Send} ->
+ [Send|Acc];
+ {send, S} ->
+ %% TODO: it would be better if
+ %% Result was never of the
+ %% {send, ...} variety because
+ %% it requires us to take the
+ %% content out via searching.
+ case lists:keysearch(content,1,S) of
+ {value, {content, _, Send}} ->
+ [Send|Acc];
+ _ ->
+ Acc
+ end
+ end
+ end, [], Batch),
+ case BatchRes of
+ [] ->
+ %% all notifications, no replies
+ send(Args, 200, json);
+ _ ->
+ send(Args, 200,
+ "["++yaws:join_sep(lists:reverse(BatchRes),",")++"]",
+ [], json)
+ end;
+ NonBatch ->
+ Result = check_decoded_payload(Args, Handler, NonBatch,
+ Payload, Type, RpcType),
+ case Result of
+ {send, Send} ->
+ Send;
+ empty ->
+ send(Args, 200, RpcType);
+ {result, Code, Send} ->
+ send(Args, Code, Send, [], RpcType)
+ end
+ end.
+
+check_decoded_payload(Args, Handler, DecodedResult, Payload, Type, RpcType) ->
+ case DecodedResult of
{ok, DecodedPayload, ID} ->
- ?Debug("client2erl decoded call ~p ~n", [DecodedPayload]),
+ ?Debug("client2erl decoded call ~p~n", [DecodedPayload]),
eval_payload(Args, Handler, DecodedPayload, Type, ID, RpcType);
{error, Reason} ->
?ERROR_LOG({html, client2erl, Payload, Reason}),
- send(Args, 400, RpcType)
+ case RpcType of
+ json ->
+ case Reason of
+ {ErrCode, _ErrString} ->
+ {result, 200, json_error(ErrCode)};
+ ErrCode ->
+ {result, 200, json_error(ErrCode)}
+ end;
+ _ ->
+ {send, send(Args, 400, RpcType)}
+ end
end.
-%%% Identify the RPC type. We first try recognize haXe by the
+%%% Identify the RPC type. We first try to recognize haXe by the
%%% "X-Haxe-Remoting" HTTP header, then the "SOAPAction" header,
%%% and if those are absent we assume the request is JSON.
recognize_rpc_type(Args) ->
@@ -143,7 +205,6 @@ recognize_rpc_hdr([{_,_,"soapaction",_,_}|_]) -> soap;
recognize_rpc_hdr([_|T]) -> recognize_rpc_hdr(T);
recognize_rpc_hdr([]) -> json.
-
%%%
%%% call handler/3 and provide session support
eval_payload(Args, {M, F}, Payload, {session, CookieName}, ID, RpcType) ->
@@ -162,33 +223,41 @@ eval_payload(Args, {M, F}, Payload, {session, CookieName}, ID, RpcType) ->
end,
CbackFun = callback_fun(M, F, Args, Payload, SessionValue, RpcType),
case catch CbackFun() of
+ {'EXIT', {function_clause, _}} when RpcType == json ->
+ case ID of
+ undefined ->
+ %% empty HTTP reply for notification
+ empty;
+ _ ->
+ {result, 200, json_error(-32601, ID)}
+ end;
{'EXIT', Reason} ->
?ERROR_LOG({M, F, {'EXIT', Reason}}),
- send(Args, 500, RpcType);
+ {send, send(Args, 500, RpcType)};
{error, Reason} ->
?ERROR_LOG({M, F, Reason}),
- send(Args, 500, RpcType);
+ {send, send(Args, 500, RpcType)};
{error, Reason, Rc} ->
?ERROR_LOG({M, F, Reason}),
- send(Args, Rc, Reason, [], RpcType);
+ {send, send(Args, Rc, Reason, [], RpcType)};
{false, ResponsePayload} ->
%% do not have updates in session data
- encode_send(Args, 200, ResponsePayload, [], ID, RpcType);
+ {send, encode_send(Args, 200, ResponsePayload, [], ID, RpcType)};
{false, ResponsePayload, RespCode} ->
%% do not have updates in session data
- encode_send(Args, RespCode, ResponsePayload, [], ID, RpcType);
- false -> % soap notify
- false;
+ {send, encode_send(Args,RespCode,ResponsePayload,[],ID,RpcType)};
+ false -> % soap or json-rpc notify
+ empty;
{true, _NewTimeout, NewSessionValue, ResponsePayload} ->
%% be compatible with xmlrpc module
CO = handle_cookie(Cookie, CookieName, SessionValue,
NewSessionValue, M, F),
- encode_send(Args, 200, ResponsePayload, CO, ID, RpcType);
+ {send, encode_send(Args, 200, ResponsePayload, CO, ID, RpcType)};
{true, _NewTimeout, NewSessionValue, ResponsePayload, RespCode} ->
%% be compatible with xmlrpc module
CO = handle_cookie(Cookie, CookieName, SessionValue,
NewSessionValue, M, F),
- encode_send(Args, RespCode, ResponsePayload, CO, ID, RpcType)
+ {send, encode_send(Args, RespCode, ResponsePayload, CO, ID, RpcType)}
end;
%%%
@@ -198,16 +267,16 @@ eval_payload(Args, {M, F}, Payload, simple, ID, RpcType) ->
case catch M:F(Args#arg.state, Payload) of
{'EXIT', Reason} ->
?ERROR_LOG({M, F, {'EXIT', Reason}}),
- send(Args, 500);
+ {send, send(Args, 500)};
{error, Reason} ->
?ERROR_LOG({M, F, Reason}),
- send(Args, 500);
+ {send, send(Args, 500)};
{false, ResponsePayload} ->
- encode_send(Args, 200, ResponsePayload, [], ID, RpcType);
- false -> % Soap notify !?
- false;
+ {send, encode_send(Args, 200, ResponsePayload, [], ID, RpcType)};
+ false -> % Soap notify
+ {send, send(Args, 200, RpcType)};
{true, _NewTimeout, _NewState, ResponsePayload} ->
- encode_send(Args, 200, ResponsePayload, [], ID, RpcType)
+ {send, encode_send(Args, 200, ResponsePayload, [], ID, RpcType)}
end.
handle_cookie(Cookie, CookieName, SessionValue, NewSessionValue, M, F) ->
@@ -243,85 +312,71 @@ get_expire(M, F) ->
_ -> false
end.
-callback_fun(M, F, Args, Payload, SessionValue, RpcType) when RpcType==soap ->
+callback_fun(M, F, Args, Payload, SessionValue, soap) ->
fun() -> yaws_soap_srv:handler(Args, {M,F}, Payload, SessionValue) end;
callback_fun(M, F, Args, Payload, SessionValue, _RpcType) ->
fun() -> M:F(Args#arg.state, Payload, SessionValue) end.
-
%%% XXX compatibility with XMLRPC handlers
%%% XXX - potential bug here?
encode_send(Args, StatusCode, [Payload], AddOn, ID, RpcType) ->
encode_send(Args, StatusCode, Payload, AddOn, ID, RpcType);
-encode_send(_Args, _StatusCode, Payload, _AddOn, ID, RpcType) ->
+encode_send(Args, StatusCode, Payload, _AddOn, ID, RpcType) ->
?Debug("rpc response ~p ~n", [Payload]),
EncodedPayload = encode_handler_payload(Payload, ID, RpcType),
?Debug("rpc encoded response ~p ~n", [EncodedPayload]),
- EncodedPayload.
+ send(Args, StatusCode, EncodedPayload, [], RpcType).
-send(Args, StatusCode) -> send(Args, StatusCode, json).
+send(Args, StatusCode) ->
+ send(Args, StatusCode, json).
-send(Args, StatusCode, RpcType) -> send(Args, StatusCode, "", [], RpcType).
+send(Args, StatusCode, RpcType) ->
+ send(Args, StatusCode, "", [], RpcType).
send(_Args, StatusCode, Payload, AddOnData, RpcType) ->
- A = [
- {status, StatusCode},
- content_hdr(RpcType, Payload),
- {header, {content_length, lists:flatlength(Payload) }}
- ] ++ AddOnData,
- A.
+ [{status, StatusCode},
+ content_hdr(RpcType, Payload),
+ {header, {content_length, lists:flatlength(Payload)}}] ++ AddOnData.
content_hdr(json, Payload) -> {content, "application/json", Payload};
-content_hdr(_, Payload) -> {content, "text/xml", Payload}.
+content_hdr(_, Payload) -> {content, "application/xml", Payload}.
%% FIXME would like to add charset info here !!
encode_handler_payload({Xml,[]}, _ID, soap) ->
Xml;
-
encode_handler_payload(Xml, _ID, soap) ->
Xml;
-
encode_handler_payload({error, [ErlStruct]}, ID, RpcType) ->
encode_handler_payload({error, ErlStruct}, ID, RpcType);
-
-
encode_handler_payload({error, ErlStruct}, ID, RpcType) ->
StructStr =
case RpcType of
- json -> json2:encode({struct, [{result, null}, {id, ID},
- {error, ErlStruct}]});
+ json -> json2:encode({struct, [{id, ID}, {error, ErlStruct},
+ {"jsonrpc", "2.0"}]});
haxe -> [$h, $x, $r | haxe:encode({exception, ErlStruct})]
end,
StructStr;
-
encode_handler_payload({response, [ErlStruct]}, ID, RpcType) ->
encode_handler_payload({response, ErlStruct}, ID, RpcType);
-
-
encode_handler_payload({response, ErlStruct}, ID, RpcType) ->
StructStr =
case RpcType of
json -> json2:encode({struct, [{result, ErlStruct}, {id, ID},
- {error, null}]});
+ {"jsonrpc", "2.0"}]});
haxe -> [$h, $x, $r | haxe:encode(ErlStruct)]
end,
StructStr.
-
decode_handler_payload(json, JSonStr) ->
try
{ok, Obj} = json2:decode_string(JSonStr),
- Method = list_to_atom(jsonrpc:s(Obj, method)),
- {array, Args} = jsonrpc:s(Obj, params),
- ID = jsonrpc:s(Obj, id),
- {ok, {call, Method, Args}, ID}
+ decode_handler_payload_json(Obj)
catch
error:Err ->
- ?ERROR_LOG({ json_decode , JSonStr , Err }),
- {error, Err}
+ ?ERROR_LOG({json_decode, JSonStr, Err}),
+ {error, {-32700, Err}}
end;
-
decode_handler_payload(haxe, [$_, $_, $x, $= | HaxeStr]) ->
try
{done, {ok, {array, [MethodName | _]}}, Cont} = haxe:decode(HaxeStr),
@@ -338,5 +393,66 @@ decode_handler_payload(haxe, _HaxeStr) ->
decode_handler_payload(soap, Payload) ->
{ok, Payload, undefined}.
-
-
+decode_handler_payload_json({struct, _}=Obj) ->
+ case jsonrpc:s(Obj, method) of
+ undefined ->
+ {error, -32600};
+ Method0 when is_list(Method0) ->
+ Method = case jsonrpc:s(Obj, jsonrpc) of
+ "2.0" ->
+ try
+ list_to_existing_atom(Method0)
+ catch
+ error:badarg ->
+ Method0
+ end;
+ undefined ->
+ list_to_atom(Method0)
+ end,
+ Args = jsonrpc:s(Obj, params),
+ ArgsOk = case Args of
+ {struct, _} -> true;
+ {array, _} -> true;
+ undefined -> true;
+ _ -> false
+ end,
+ case ArgsOk of
+ true ->
+ ID = jsonrpc:s(Obj, id),
+ CallOrNotify = case ID of
+ undefined ->
+ notification;
+ _ ->
+ call
+ end,
+ {ok, {CallOrNotify, Method, Args}, ID};
+ false ->
+ {error, -32602}
+ end;
+ _ ->
+ {error, -32600}
+ end;
+decode_handler_payload_json({array, []}) ->
+ {error, -32600};
+decode_handler_payload_json({array, Batch}) ->
+ [decode_handler_payload_json(Obj) || Obj <- Batch];
+decode_handler_payload_json(_) ->
+ {error, -32600}.
+
+json_error(ErrCode) ->
+ json_error(ErrCode, null).
+json_error(ErrCode, Id) ->
+ Err = {struct, [{"jsonrpc", "2.0"},
+ {"id", Id},
+ {"error", {struct,
+ [{"code", ErrCode},
+ {"message", json_error_message(ErrCode)}]}}]},
+ json2:encode(Err).
+
+json_error_message(-32700) -> "parse error";
+json_error_message(-32600) -> "invalid request";
+json_error_message(-32601) -> "method not found";
+json_error_message(-32602) -> "invalid params";
+json_error_message(-32603) -> "internal error";
+json_error_message(Code) when Code >= -32099, Code =< -32000 -> "server error";
+json_error_message(_) -> "json error".
View
6 test/Makefile
@@ -2,8 +2,7 @@ include support/include.mk
SUBDIRS = t1 t2 eunit
-all: conf fetch_ibrowse
- @cd ibrowse; $(MAKE)
+all: conf ibrowse
@cd src; $(MAKE) all
@set -e ; \
for d in $(SUBDIRS) ; do \
@@ -30,6 +29,9 @@ test:
fi ; \
done
+ibrowse: fetch_ibrowse
+ @cd ibrowse; $(MAKE)
+
IBROWSE_URI = https://github.com/cmullaparthi/ibrowse.git
IBROWSE_DOWNLOAD_URI = https://github.com/cmullaparthi/ibrowse/tarball/master
IBROWSE_TGZ = ibrowse.tar.gz
View
22 test/conf/stdconf.conf
@@ -20,9 +20,9 @@ trace = false
-# it is possible to have yaws start additional
+# it is possible to have yaws start additional
# application specific code at startup
-#
+#
# runmod = mymodule
@@ -46,15 +46,15 @@ log_resolve_hostname = false
-# fail completely or not if yaws fails
+# fail completely or not if yaws fails
# to bind a listen socket
fail_on_bind_err = true
# If yaws is started as root, it can, once it has opened
-# all relevant sockets for listening, change the uid to a
-# user with lower accessrights than root
+# all relevant sockets for listening, change the uid to a
+# user with lower accessrights than root
# username = nobody
@@ -65,7 +65,7 @@ fail_on_bind_err = true
auth_log = true
-# When we're running multiple yaws systems on the same
+# When we're running multiple yaws systems on the same
# host, we need to give each yaws system an individual
# name. Yaws will write a number of runtime files under
# /tmp/yaws/${id}
@@ -86,7 +86,7 @@ pick_first_virthost_on_nomatch = true
# All unices are broken since it's not possible to bind to
# a privileged port (< 1024) unless uid==0
-# There is a contrib in jungerl which makes it possible by means
+# There is a contrib in jungerl which makes it possible by means
# of an external setuid root programm called fdsrv to listen to
# to privileged port.
# If we use this feature, it requires fdsrv to be properly installed.
@@ -141,3 +141,11 @@ use_fdsrv = false
docroot = /tmp
appmods = <non_root_appmod, app_test>
</server>
+
+<server localhost>
+ port = 8005
+ listen = 0.0.0.0
+ docroot = /tmp
+ appmods = </, jsontest>
+</server>
+
View
2 test/t2/Makefile
@@ -3,7 +3,7 @@ include ../support/include.mk
.PHONY: all test debug clean
#
-all: conf setup app_test.beam streamtest.beam
+all: conf setup app_test.beam streamtest.beam jsontest.beam
@echo "all ok"
View
221 test/t2/app_test.erl
@@ -20,6 +20,7 @@ start() ->
appmod_test(),
streamcontent_test(),
sendfile_get(),
+ json_test(),
ibrowse:stop().
@@ -266,6 +267,226 @@ streamcontent_test() ->
gen_tcp:close(Sock),
ok.
+json_test() ->
+ io:format("json_test\n",[]),
+ io:format(" param array1\n", []),
+ ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
+ {"method", "subtract"},
+ {"params", {array, [42, 23]}},
+ {"id", 1}]},
+ {struct, [{"jsonrpc", "2.0"},
+ {"result", 19},
+ {"id", 1}]}),
+ io:format(" param array2\n", []),
+ ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
+ {"method", "subtract"},
+ {"params", {array, [23, 42]}},
+ {"id", 2}]},
+ {struct, [{"jsonrpc", "2.0"},
+ {"result", -19},
+ {"id", 2}]}),
+ io:format(" param obj1\n", []),
+ ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
+ {"method", "subtract"},
+ {"params", {struct, [{"subtrahend", 23},
+ {"minuend", 42}]}},
+ {"id", 3}]},
+ {struct, [{"jsonrpc", "2.0"},
+ {"result", 19},
+ {"id", 3}]}),
+ io:format(" param obj2\n", []),
+ ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
+ {"method", "subtract"},
+ {"params", {struct, [{"minuend", 42},
+ {"subtrahend", 23}]}},
+ {"id", 4}]},
+ {struct, [{"jsonrpc", "2.0"},
+ {"result", 19},
+ {"id", 4}]}),
+ io:format(" notif1\n", []),
+ ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
+ {"method", "update"},
+ {"params", {array, [1,2,3,4,5]}}]},
+ notification),
+ io:format(" notif2\n", []),
+ ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
+ {"method", "foobar"}]},
+ notification),
+ io:format(" missing method\n", []),
+ ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
+ {"method", "foobar"},
+ {"id", "1"}]},
+ {struct, [{"jsonrpc", "2.0"},
+ {"id", "1"},
+ {"error", {struct,
+ [{"code", -32601},
+ {"message", "method not found"}]}
+ }]}),
+ io:format(" invalid json\n", []),
+ InvalidJson = "{\"jsonrpc\": \"2.0\", \"method\": \"foobar,"
+ "\"params\": \"bar\", \"baz]",
+ ?line ok = do_json(InvalidJson,
+ {struct, [{"jsonrpc", "2.0"},
+ {"id", null},
+ {"error", {struct,
+ [{"code", -32700},
+ {"message", "parse error"}]}
+ }]},
+ no_encode),
+ io:format(" invalid req1\n", []),
+ InvalidReq1 = "{\"jsonrpc\": \"2.0\", \"method\": 1, \"params\": \"bar\"}",
+ ?line ok = do_json(InvalidReq1,
+ {struct, [{"jsonrpc", "2.0"},
+ {"id", null},
+ {"error", {struct,
+ [{"code", -32600},
+ {"message", "invalid request"}]}
+ }]},
+ no_encode),
+ io:format(" invalid params\n", []),
+ InvalidReq2 = "{\"jsonrpc\": \"2.0\", \"method\": \"x\","
+ "\"params\": \"bar\"}",
+ ?line ok = do_json(InvalidReq2,
+ {struct, [{"jsonrpc", "2.0"},
+ {"id", null},
+ {"error", {struct,
+ [{"code", -32602},
+ {"message", "invalid params"}]}
+ }]},
+ no_encode),
+ io:format(" invalid batch json\n", []),
+ InvalidJsonBatch = "[ {\"jsonrpc\": \"2.0\", \"method\": \"sum\","
+ "\"params\": [1,2,4],\"id\": \"1\"},{\"jsonrpc\": \"2.0\", \"method\" ]",
+ ?line ok = do_json(InvalidJsonBatch,
+ {struct, [{"jsonrpc", "2.0"},
+ {"id", null},
+ {"error", {struct,
+ [{"code", -32700},
+ {"message", "parse error"}]}
+ }]},
+ no_encode),
+ io:format(" empty batch\n", []),
+ EmptyBatch = "[]",
+ ?line ok = do_json(EmptyBatch,
+ {struct, [{"jsonrpc", "2.0"},
+ {"id", null},
+ {"error", {struct,
+ [{"code", -32600},
+ {"message", "invalid request"}]}
+ }]},
+ no_encode),
+ io:format(" invalid batch1\n", []),
+ BogusBatch1 = "[1]",
+ ?line ok = do_json(BogusBatch1,
+ {array,
+ [{struct, [{"jsonrpc", "2.0"},
+ {"id", null},
+ {"error", {struct,
+ [{"code", -32600},
+ {"message", "invalid request"}]}
+ }]}]},
+ no_encode),
+ io:format(" invalid batch2\n", []),
+ BogusBatch2 = "[1,2,3]",
+ ?line ok = do_json(BogusBatch2,
+ {array,
+ [{struct, [{"jsonrpc", "2.0"},
+ {"id", null},
+ {"error", {struct,
+ [{"code", -32600},
+ {"message", "invalid request"}]}
+ }]},
+ {struct, [{"jsonrpc", "2.0"},
+ {"id", null},
+ {"error", {struct,
+ [{"code", -32600},
+ {"message", "invalid request"}]}
+ }]},
+ {struct, [{"jsonrpc", "2.0"},
+ {"id", null},
+ {"error", {struct,
+ [{"code", -32600},
+ {"message", "invalid request"}]}
+ }]}]},
+ no_encode),
+ io:format(" mixed batch\n", []),
+ MixedBatch = "
+ [{\"jsonrpc\":\"2.0\",\"method\":\"sum\",\"params\": [1,2,4], \"id\": \"1\"},
+ {\"jsonrpc\":\"2.0\",\"method\":\"notify_hello\", \"params\": [7]},
+ {\"jsonrpc\":\"2.0\",\"method\":\"subtract\",\"params\":[42,23],
+ \"id\":\"2\"},
+ {\"foo\": \"boo\"},
+ {\"jsonrpc\":\"2.0\",\"method\":\"foo.get\",
+ \"params\":{\"name\": \"myself\"}, \"id\": \"5\"},
+ {\"jsonrpc\": \"2.0\", \"method\": \"get_data\", \"id\": \"9\"}]",
+ ?line ok = do_json(MixedBatch,
+ {array,
+ [{struct,[{"jsonrpc","2.0"},
+ {"result",7},
+ {"id","1"}]},
+ {struct,[{"jsonrpc","2.0"},
+ {"result",19},{"id","2"}]},
+ {struct,[{"jsonrpc","2.0"},
+ {"error",
+ {struct,[{"code",-32600},
+ {"message","invalid request"}]}},
+ {"id",null}]},
+ {struct,[{"jsonrpc","2.0"},
+ {"error",
+ {struct,[{"code",-32601},
+ {"message","method not found"}]}},
+ {"id","5"}]},
+ {struct,[{"jsonrpc","2.0"},
+ {"result",{array,["hello",5]}},
+ {"id","9"}]}]},
+ no_encode),
+ io:format(" all-notification batch\n", []),
+ NotifBatch = "[
+ {\"jsonrpc\": \"2.0\", \"method\": \"notify_sum\", \"params\": [1,2,4]},
+ {\"jsonrpc\": \"2.0\", \"method\": \"notify_hello\", \"params\": [7]}]",
+ ?line ok = do_json(NotifBatch, notification, no_encode),
+ ok.
+
+do_json(Req, Expected) ->
+ do_json(Req, Expected, encode).
+do_json(Req, notification, NeedEncode) ->
+ ?line {ok, "200", Headers, Body} = json_send(Req, NeedEncode),
+ ?line "application/json" = proplists:get_value("Content-Type", Headers),
+ ?line [] = Body,
+ ok;
+do_json(Req, {struct, _}=Expected, NeedEncode) ->
+ ?line {ok, "200", Headers, Body} = json_send(Req, NeedEncode),
+ ?line "application/json" = proplists:get_value("Content-Type", Headers),
+ check_json(Expected, Body, true);
+do_json(Req, {array, Array}, NeedEncode) ->
+ ?line {ok, "200", Headers, Body} = json_send(Req, NeedEncode),
+ ?line "application/json" = proplists:get_value("Content-Type", Headers),
+ ?line {ok, {array, GotArray}} = json2:decode_string(Body),
+ lists:map(fun({Obj, Got}) ->
+ ?line ok = check_json(Obj, Got, false)
+ end, lists:zip(Array, GotArray)),
+ ok.
+
+check_json({struct, _}=Exp, Body, true) ->
+ ?line {ok, DecodedBody} = json2:decode_string(Body),
+ check_json(Exp, DecodedBody);
+check_json({struct, _}=Exp, Body, false) ->
+ check_json(Exp, Body).
+check_json({struct, Members}, DecodedBody) ->
+ lists:foreach(fun({Key, Val}) ->
+ ?line Val = jsonrpc:s(DecodedBody, Key)
+ end, Members),
+ ok.
+
+json_send(Req) ->
+ json_send(Req, encode).
+json_send(Req, encode) ->
+ json_send(json2:encode(Req), no_encode);
+json_send(Req, no_encode) ->
+ Uri = "http://localhost:8005/jsontest",
+ ReqHdrs = [{content_type, "application/json"}],
+ ibrowse:send_req(Uri, ReqHdrs, post, Req).
+
recv_hdrs(Sock) ->
recv_hdrs(Sock, 0).
recv_hdrs(Sock, Len) ->
View
28 test/t2/jsontest.erl
@@ -0,0 +1,28 @@
+-module(jsontest).
+-export([out/1, handler/3]).
+
+out(Arg) ->
+ yaws_rpc:handler_session(Arg, {?MODULE, handler}).
+
+handler(_State, {call, subtract, Params}, _Session) ->
+ {Minuend, Subtrahend} = case Params of
+ {array, [M, S]} ->
+ {M, S};
+ Obj ->
+ {jsonrpc:s(Obj, "minuend"),
+ jsonrpc:s(Obj, "subtrahend")}
+ end,
+ {true, undefined, undefined, {response, Minuend - Subtrahend}};
+handler(_State, {notification, update, {array, [1,2,3,4,5]}}, _Session) ->
+ false;
+handler(_State, {notification, foobar, undefined}, _Session) ->
+ false;
+handler(_State, {call, sum, {array, Params}}, _Session) ->
+ {true, undefined, undefined, {response, lists:sum(Params)}};
+handler(_State, {notification, "notify_hello", {array, [7]}}, _Session) ->
+ false;
+handler(_State, {call, get_data, undefined}, _Session) ->
+ {true, undefined, undefined, {response, {array, ["hello", 5]}}}.
+
+
+
View
180 www/json_intro.yaws
@@ -19,67 +19,76 @@ ss(A, File) ->
{ok, B} = file:read_file(
filename:join([A#arg.docroot, File])),
box(binary_to_list(B)).
-
-out(A) ->
+
+out(A) ->
[{ssi, "TAB.inc", "%%",[{"json_intro", "choosen"}]},
{ehtml,
{'div', [{id, "entry"}],
[{h1, [], "AJAX through JSON RPC"},
-
+
{p, [],
- {i, [],
+ {i, [],
["Note: this documentation used to refer to the module "
- "'yaws_jsonrpc'. This module has been depracated in favor "
- "of 'yaws_rpc', which handles both JSON RPC, haXe and SOAP "
- "remoting. All references to 'yaws_jsonrpc' on this page "
- "were therefore changed to 'yaws_rpc'. For more specific "
- "information about SOAP, refer to ",
+ "'yaws_jsonrpc', but that module was deprecated in favor of "
+ "'yaws_rpc', which handles JSON RPC, haXe and SOAP remoting. "
+ "For more specific information about SOAP, refer to ",
{a, [{href, "/soap_intro.yaws"}], "the SOAP page."}]}},
{p, [],
- ["The Yaws Json binding is a way to have Javascript code in the "
- "browser evaluate a remote procedure call in the Yaws server."
+ ["The Yaws JSON-RPC binding is a way to have JavaScript code in the "
+ "browser evaluate a remote procedure call (RPC) in the Yaws server. "
"JSON itself as described at ",
{a, [{href, "http://www.json.org/"}], "http://www.json.org/ "},
"is basically a simple marshaling format which can be used "
- " from a variety of different programming languages, in particular "
- " it completely straightforward to implement in Javascript."]},
+ "from a variety of different programming languages, and "
+ "naturally it's completely straightforward to implement "
+ "in JavaScript itself. JSON-RPC version 2.0, the version Yaws "
+ "supports, is described here:"]},
+ {p, [],
+ [{a, [{href,
+ "http://groups.google.com/group/json-rpc/web/json-rpc-2-0"}],
+ "http://groups.google.com/group/json-rpc/web/json-rpc-2-0"}]},
{p, [],
- "The yaws JSON implementation consist of Javascript client and a "
- " server side library which must be explicitly invoked by Erlang "
- "code in a .yaws page."},
+ "The Yaws JSON-RPC implementation consist of JavaScript clients and a "
+ "server side library that must be explicitly invoked by Erlang "
+ "code in a .yaws page, appmod, etc."},
{p,[],
"It is not particularly easy to show and explain an AJAX setup "
- "through JSON RPC, but here is an attempt:"
+ "through JSON-RPC, but here is an attempt:"
},
- {p,[],
+ {p,[],
"First we have an HTML page which:"},
{ol, [],
[
- {li,[],{p,[], "Includes the client side of the JSON library."
- " The library is included in the yaws distribution "
- " and it is found under \"www/jsolait/jsolait.js\" ."}},
- {li,[],{p,[],"Second the HTML code defines the name of a method, "
- "i.e. the name of a server side function which shall be "
- " called by the client side Javascript code"}},
- {li,[],{p,[],"Finally the HTML code defines a FORM which is "
+ {li,[],{p,[],
+ ["Includes the client side of the JSON library. "
+ "The library is included in the Yaws distribution "
+ "and it is found under ",
+ {a,
+ [{href,
+ "https://github.com/klacke/yaws/blob/master/www/jsolait/jsolait.js"}],
+ "\"www/jsolait/jsolait.js\""}, "."]}},
+ {li,[],{p,[],"Second, the HTML code defines the name of a method, "
+ "i.e. the name of a server-side function that shall be "
+ "called by the client side JavaScript code."}},
+ {li,[],{p,[],"Finally the HTML code defines a FORM that's "
"used to invoke the RPC. This is just a really simple "
- "example, really any Javascript code can invoke any RPC in "
- "more interesting scenarios than submitting a form"}}]},
-
- {p, [], "The HTML code looks like "},
+ "example, really any JavaScript code can invoke any RPC in "
+ "more interesting scenarios than submitting a form."}}]},
+
+ {p, [], "The HTML code appears as shown below:"},
ss(A,"json_sample.html"),
{p, [], ["This HTML code resides in file ",
{a,[{href, "json_sample.html"}], "json_sample.html"},
- " and it is the HTML code that is the AJAX GUI !!!"]},
- {p, [], "Following that we need to take a look at the page "
- "json_sample.yaws which is the \"serviceURL\" according to "
- "the Javascript code. This code defines the function to be "
- "called. Remember that the Javascript code defined one method, "
- "called \"test1\", this information will be passed to the "
+ " and it is the HTML code that is the AJAX GUI."]},
+ {p, [], "Following that we need to take a look at json_sample.yaws "
+ " (shown below), which is the \"serviceURL\" according to "
+ "the JavaScript code. This code defines the function to be "
+ "called. Remember that the JavaScript code defined one method, "
+ "called \"test1\"; this information will be passed to the "
"serviceURL. The code looks like:"},
ss(A, "json_sample.yaws"),
@@ -91,15 +100,20 @@ out(A) ->
{li,[],
{pre,[],"counter([{ip, IP}] = _State, {call, test1, Value} = _Request, Session)"}}]},
- {p,[],
- "The first line tells Yaws to forward all JSON-RPC methods to the "
- " \"counter\" function in the \"sample_mod\" module. "
- "The second line says, "
- " basically, - this is the counter function that will be called when "
- " the client invokes a method called 'test1'. We would duplicate "
- " this line with a different name than 'test1' for each RPC function "
- "we wish to implement."},
-
+ {p,[],
+ ["The first line tells Yaws to forward all JSON-RPC methods to the "
+ " \"counter\" function in the \"sample_mod\" module. "
+ "The second line is the head of the counter function that will be "
+ "called when the client invokes a method called 'test1'. We would "
+ "duplicate this line with a different name than 'test1' for each RPC "
+ "function we wish to implement. Note that the first atom in the "
+ "request tuple will either be 'call' or 'notification' to indicate "
+ "the type of request. As per the ",
+ {a,[{href,"http://groups.google.com/group/json-rpc/web/json-rpc-2-0"}],
+ "JSON-RPC 2.0 specification"},
+ ", a 'call' is a regular request-reply while a 'notification' is a "
+ "one-way message that does not have a corresponding reply."]},
+
{p,[],"On the client side we have"},
box("
@@ -108,50 +122,45 @@ var jsonrpc = imprt(\"jsonrpc\");
var service = new jsonrpc.ServiceProxy(serviceURL, methods);
"),
- {p,[],"Which registers the Yaws page with the JSON-RPC handler and "
+ {p,[],"This registers the Yaws page with the JSON-RPC handler and "
"gives it a list of methods that the Yaws page can satisfy. "
- "In this case, it is only the method called 'test1'."},
-
- {p, [],
-"When we wish to return structured data - we simply let "
-"the user defined RPC function return JSON structures such as "},
+ "In this case, the only method called 'test1'."},
+
+ {p, [],
+"When we wish to return structured data, we simply let "
+"the user-defined RPC function return JSON structures such as "},
-box(" {struct, [{field1, \"foo\"}, {field2, \"bar\"}]} "),
+box("{struct, [{field1, \"foo\"}, {field2, \"bar\"}]} "),
{p, [], " for a structure and "},
box("{array, [\"foo\", \"bar\"]}"),
-{p, [],"for an array. We can nest arrays and structs in each other "},
-
-
-
-
+{p, [],"for an array. We can nest arrays and structs in each other."},
{p, [],
"Finally, we must stress that this example is extremely simple. "
"In order to build a proper AJAX application in Yaws, a lot of "
"client side work is required, all Yaws provides is the basic "
- "mechanism whereby the client side Javascript code, can RPC the "
+ "mechanism whereby the client side JavaScript code can RPC the "
"web server for data which can be subsequently used to populate "
"the DOM. Also required to build a good AJAX application is "
- "good knowledge on how the DOM in the browser works"},
+ "good knowledge of how the DOM in the browser works"},
{p, [],
- [{b,[],"Update:"},
- "The yaws_rpc:handler will now also call: M:F(cookie_expire) which is "
- "expected to return a proper Cookie expire string. This makes it possible "
- "to setup the Cookie lifetime. If this calback function is non-existant, "
- "the default behaviour is to not set a cookie expiration time, i.e it will "
- "live for this session only."]},
+ ["The yaws_rpc:handler will also call: M:F(cookie_expire) "
+ "which is expected to return a proper Cookie expire string. This "
+ "makes it possible to setup the Cookie lifetime. If this callback "
+ "function is non-existent, the default behaviour is to not set a "
+ "cookie expiration time, i.e., it will live for this session only."]},
{h3, [], "One more example "},
{p, [],
["Here is yet another example, stolen from ",
- {a,
- [{href,"http://blog.tornkvist.org/blog.yaws?id=1179347231289901#"}],
- "Tobbes blog"}
+ {a,
+ [{href,"http://www.redhoterlang.com/entry/ac061493b201e3d1b4490cdc3f911068"}],
+ "Tobbe's blog."}
]},
- {h4, [], "Setup the DOM"},
- {p, [], "In the file ''ex1.html'' we create the DOM with a little HTML and add some Javascript that will talk with the Erlang server side."},
+ {h4, [], "Setup the DOM"},
+ {p, [], "In the file ''ex1.html'' we create the DOM with a little HTML and add some JavaScript that will talk with the Erlang server side."},
box("
<html>
@@ -204,7 +213,7 @@ out(A) ->
L = yaws_api:parse_query(A),
dispatch(lkup(\"op\", L, false), A, L).
-dispatch(\"ex1\", A, L) ->
+dispatch(\"ex1\", A, L) ->
ex1(A, L).
ex1(_A, L) ->
@@ -222,7 +231,7 @@ two() -> obj(\"two\").
three() -> obj(\"three\").
obj(M) ->
- obj(M, \"r\").
+ obj(M, \"r\").
%%%
%%% How ::= \"r\" | \"a\" , r=replace, a=append
@@ -235,8 +244,8 @@ obj(M, How) ->
{\"what\", C ++\" \"++M++\" content\"}]}].
return_json(Json) ->
- {content,
- \"application/json; charset=iso-8859-1\",
+ {content,
+ \"application/json; charset=iso-8859-1\",
Json}.
now2str() ->
@@ -254,27 +263,18 @@ lkup(Key, List, Def) ->
") ,
{h2, [], "The json library"},
- {p, [], "The Yaws JSON library contains 3 simple functions, "
- " One to encode and two for decoding. See source code json2.erl "
- " for detailed instructions on usage "}
-
-
-
-
+ {p, [],
+ ["The Yaws JSON library contains 3 simple functions, "
+ " one for encoding and two for decoding. See source code ",
+ {a,
+ [{href,
+ "https://github.com/klacke/yaws/blob/master/src/json2.erl"}],
+ "json2.erl"},
+ " for detailed instructions on usage."]}
]}},
{ssi, "END2",[],[]}
].
-
-
-
</erl>
-
-
-
-
-
-
-

0 comments on commit 15454bc

Please sign in to comment.