Skip to content
This repository
Browse code

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...
commit 15454bcd87690f22059a0a86abc26ea7d10d4933 1 parent 96534b2
Steve Vinoski vinoski authored
87 src/jsonrpc.erl
@@ -28,85 +28,26 @@
28 28
29 29 -module(jsonrpc).
30 30 -author("Gaspar Chilingarov <nm@web.am>, Gurgen Tumanyan <barbarian@armkb.com>").
31   --vsn("2").
32   --export([call/3]).
  31 +-vsn("3").
33 32 -export([s/2]). % extract element from proplist
34 33
35   -%%%
36   -%%% call function calls json-rpc method on remote host
37   -%%%
38   -%%% URL - remote server url (may use https)
39   -%%% Options - option list to be passed to http:request
40   -%% (ssl options ot timeout, for example)
41   -%%% Payload -> {call, MethodName, Args} tuple
42   -%%% MethodName -> atom
43   -%%% Args -> list
44   -%%%
45   -call(URL, Options, Payload) ->
46   - try
47   - {ok, CallPayloadDeep} = encode_call_payload(Payload),
48   - CallPayload = lists:flatten(CallPayloadDeep),
49   - {ok, Response} = httpc:request(post,
50   - {URL,[{"Content-length",length(CallPayload)}],
51   - "application/x-www-form-urlencoded",CallPayload},
52   - Options, []),
53   -
54   - RespBody= if (size(Response) == 2) or (size(Response) == 3) ->
55   - element(size(Response), Response)
56   - end,
57   - decode_call_payload(RespBody)
58   - catch
59   - error:Err->
60   - error_logger:error_report([{'json_rpc:call', error},
61   - {error, Err},
62   - {stack, erlang:get_stacktrace()}]),
63   - {error,Err}
64   - end.
65   -
66   -%%%
67   -%%% json-rpc.org defines such structure for making call
68   -%%%
69   -%%% {"method":"methodname", "params": object, "id": integer}
70   -encode_call_payload({call, Method, Args}) when is_atom(Method) and
71   - is_list(Args) ->
72   - ID = element(3, erlang:now()), % id makes sense when there are many
73   - % requests in same
74   - % communication channel and
75   - %replies can come in random
76   - %order here it can be changed
77   - %to something less expensive
78   - Struct = json2:encode({struct, [{method, atom_to_list(Method)},
79   - {params, {array, Args}},
80   - {id, ID}]}),
81   - {ok, Struct}.
82   -
83   -%%%
84   -%%% decode response structure
85   -%%%
86   -%%% {"id":requestID,"result":object,"error":error_description}
87   -decode_call_payload(JSonStr) ->
88   - {ok, JSON} = json2:decode_string(JSonStr),
89   - Result = s(JSON, result),
90   - Error = s(JSON, error),
91   -% ID = s(JSON, id), % ignored for now
92   - if
93   - (Error =/= undefined) ->
94   - {error, Error};
95   - true ->
96   - {ok,{response,[Result]}} % make it compliant with xmlrpc response
97   - end.
98   -
99 34 %%% lookup element in proplist
100   -%%% XXX: are there ready implementation in erlang std library?
101   -s ({struct, List}, ElemName) ->
  35 +s({struct, List}, ElemName) ->
102 36 s(List, ElemName);
103   -
104 37 s(List, ElemName) when is_list(List) ->
105 38 case lists:keysearch(ElemName,1,List) of
106   - {value,{ElemName,Val}} ->
107   - Val;
108   - _ ->
109   - undefined
  39 + {value,{ElemName,Val}} ->
  40 + Val;
  41 + AtomName when is_atom(ElemName) ->
  42 + ElemList = atom_to_list(ElemName),
  43 + case lists:keysearch(ElemList,1,List) of
  44 + {value,{ElemList,Val}} ->
  45 + Val;
  46 + _ ->
  47 + undefined
  48 + end;
  49 + _ ->
  50 + undefined
110 51 end.
111 52
112 53
15 src/yaws_jsonrpc.erl
... ... @@ -1,3 +1,18 @@
  1 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  2 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  3 +%%% WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED
  4 +%%% WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED
  5 +%%%
  6 +%%% Use module yaws_rpc.erl instead
  7 +%%%
  8 +%%% This module is deprecated.
  9 +%%%
  10 +%%% Do not report problems with this module, as they will not be fixed. You
  11 +%%% should instead convert your code to use the yaws_rpc module.
  12 +%%%
  13 +%%% WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED
  14 +%%% WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED WARNING DEPRECATED
  15 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1 16 %% Copyright (C) 2003 Joakim Greben� <jocke@gleipnir.com>.
2 17 %% All rights reserved.
3 18 %%
236 src/yaws_rpc.erl
@@ -38,6 +38,7 @@
38 38 -module(yaws_rpc).
39 39 -author("Gaspar Chilingarov <nm@web.am>, Gurgen Tumanyan <barbarian@armkb.com>").
40 40 -modified_by("Yariv Sadan <yarivvv@gmail.com>").
  41 +-modified_by("Steve Vinoski <vinoski@ieee.org>").
41 42
42 43 -export([handler/2]).
43 44 -export([handler_session/2, handler_session/3]).
@@ -102,35 +103,96 @@ parse_request(Args) ->
102 103 {'POST', {1,1}} ->
103 104 ?Debug("HTTP Version 1.1~n", []),
104 105 ok;
105   - {'POST', _HTTPVersion} -> {status, 505};
106   - {_Method, {1,1}} -> {status, 501};
107   - _ -> {status, 400}
  106 + {'POST', _HTTPVersion} ->
  107 + {status, 505};
  108 + {_Method, {1,1}} ->
  109 + {status, 501};
  110 + _ ->
  111 + {status, 400}
108 112 end.
109 113
110   -handle_payload(Args, Handler, Type) -> % {{{
  114 +handle_payload(Args, Handler, Type) ->
111 115 RpcType = recognize_rpc_type(Args),
112 116 %% haXe parameters are URL encoded
  117 + PL = binary_to_list(Args#arg.clidata),
113 118 {Payload,DecodedStr} =
114 119 case RpcType of
115 120 T when T==haxe; T==json ->
116   - PL = binary_to_list(Args#arg.clidata),
117   - ?Debug("rpc plaintext call ~p ~n", [PL]),
  121 + ?Debug("rpc ~p call ~p~n", [T, PL]),
118 122 {PL, yaws_api:url_decode(PL)};
119 123 _ ->
120   - PL = binary_to_list(Args#arg.clidata),
121   - ?Debug("rpc plaintext call ~p ~n", [PL]),
  124 + ?Debug("rpc plaintext call ~p~n", [PL]),
122 125 {PL, PL}
123 126 end,
124 127 case decode_handler_payload(RpcType, DecodedStr) of
  128 + Batch when RpcType == json, is_list(Batch) ->
  129 + BatchRes =
  130 + lists:foldl(fun(Req, Acc) ->
  131 + Result = check_decoded_payload(Args, Handler,
  132 + Req, Payload,
  133 + Type, json),
  134 + case Result of
  135 + empty ->
  136 + Acc;
  137 + {result, _Code, Send} ->
  138 + [Send|Acc];
  139 + {send, S} ->
  140 + %% TODO: it would be better if
  141 + %% Result was never of the
  142 + %% {send, ...} variety because
  143 + %% it requires us to take the
  144 + %% content out via searching.
  145 + case lists:keysearch(content,1,S) of
  146 + {value, {content, _, Send}} ->
  147 + [Send|Acc];
  148 + _ ->
  149 + Acc
  150 + end
  151 + end
  152 + end, [], Batch),
  153 + case BatchRes of
  154 + [] ->
  155 + %% all notifications, no replies
  156 + send(Args, 200, json);
  157 + _ ->
  158 + send(Args, 200,
  159 + "["++yaws:join_sep(lists:reverse(BatchRes),",")++"]",
  160 + [], json)
  161 + end;
  162 + NonBatch ->
  163 + Result = check_decoded_payload(Args, Handler, NonBatch,
  164 + Payload, Type, RpcType),
  165 + case Result of
  166 + {send, Send} ->
  167 + Send;
  168 + empty ->
  169 + send(Args, 200, RpcType);
  170 + {result, Code, Send} ->
  171 + send(Args, Code, Send, [], RpcType)
  172 + end
  173 + end.
  174 +
  175 +check_decoded_payload(Args, Handler, DecodedResult, Payload, Type, RpcType) ->
  176 + case DecodedResult of
125 177 {ok, DecodedPayload, ID} ->
126   - ?Debug("client2erl decoded call ~p ~n", [DecodedPayload]),
  178 + ?Debug("client2erl decoded call ~p~n", [DecodedPayload]),
127 179 eval_payload(Args, Handler, DecodedPayload, Type, ID, RpcType);
128 180 {error, Reason} ->
129 181 ?ERROR_LOG({html, client2erl, Payload, Reason}),
130   - send(Args, 400, RpcType)
  182 + case RpcType of
  183 + json ->
  184 + case Reason of
  185 + {ErrCode, _ErrString} ->
  186 + {result, 200, json_error(ErrCode)};
  187 + ErrCode ->
  188 + {result, 200, json_error(ErrCode)}
  189 + end;
  190 + _ ->
  191 + {send, send(Args, 400, RpcType)}
  192 + end
131 193 end.
132 194
133   -%%% Identify the RPC type. We first try recognize haXe by the
  195 +%%% Identify the RPC type. We first try to recognize haXe by the
134 196 %%% "X-Haxe-Remoting" HTTP header, then the "SOAPAction" header,
135 197 %%% and if those are absent we assume the request is JSON.
136 198 recognize_rpc_type(Args) ->
@@ -143,7 +205,6 @@ recognize_rpc_hdr([{_,_,"soapaction",_,_}|_]) -> soap;
143 205 recognize_rpc_hdr([_|T]) -> recognize_rpc_hdr(T);
144 206 recognize_rpc_hdr([]) -> json.
145 207
146   -
147 208 %%%
148 209 %%% call handler/3 and provide session support
149 210 eval_payload(Args, {M, F}, Payload, {session, CookieName}, ID, RpcType) ->
@@ -162,33 +223,41 @@ eval_payload(Args, {M, F}, Payload, {session, CookieName}, ID, RpcType) ->
162 223 end,
163 224 CbackFun = callback_fun(M, F, Args, Payload, SessionValue, RpcType),
164 225 case catch CbackFun() of
  226 + {'EXIT', {function_clause, _}} when RpcType == json ->
  227 + case ID of
  228 + undefined ->
  229 + %% empty HTTP reply for notification
  230 + empty;
  231 + _ ->
  232 + {result, 200, json_error(-32601, ID)}
  233 + end;
165 234 {'EXIT', Reason} ->
166 235 ?ERROR_LOG({M, F, {'EXIT', Reason}}),
167   - send(Args, 500, RpcType);
  236 + {send, send(Args, 500, RpcType)};
168 237 {error, Reason} ->
169 238 ?ERROR_LOG({M, F, Reason}),
170   - send(Args, 500, RpcType);
  239 + {send, send(Args, 500, RpcType)};
171 240 {error, Reason, Rc} ->
172 241 ?ERROR_LOG({M, F, Reason}),
173   - send(Args, Rc, Reason, [], RpcType);
  242 + {send, send(Args, Rc, Reason, [], RpcType)};
174 243 {false, ResponsePayload} ->
175 244 %% do not have updates in session data
176   - encode_send(Args, 200, ResponsePayload, [], ID, RpcType);
  245 + {send, encode_send(Args, 200, ResponsePayload, [], ID, RpcType)};
177 246 {false, ResponsePayload, RespCode} ->
178 247 %% do not have updates in session data
179   - encode_send(Args, RespCode, ResponsePayload, [], ID, RpcType);
180   - false -> % soap notify
181   - false;
  248 + {send, encode_send(Args,RespCode,ResponsePayload,[],ID,RpcType)};
  249 + false -> % soap or json-rpc notify
  250 + empty;
182 251 {true, _NewTimeout, NewSessionValue, ResponsePayload} ->
183 252 %% be compatible with xmlrpc module
184 253 CO = handle_cookie(Cookie, CookieName, SessionValue,
185 254 NewSessionValue, M, F),
186   - encode_send(Args, 200, ResponsePayload, CO, ID, RpcType);
  255 + {send, encode_send(Args, 200, ResponsePayload, CO, ID, RpcType)};
187 256 {true, _NewTimeout, NewSessionValue, ResponsePayload, RespCode} ->
188 257 %% be compatible with xmlrpc module
189 258 CO = handle_cookie(Cookie, CookieName, SessionValue,
190 259 NewSessionValue, M, F),
191   - encode_send(Args, RespCode, ResponsePayload, CO, ID, RpcType)
  260 + {send, encode_send(Args, RespCode, ResponsePayload, CO, ID, RpcType)}
192 261 end;
193 262
194 263 %%%
@@ -198,16 +267,16 @@ eval_payload(Args, {M, F}, Payload, simple, ID, RpcType) ->
198 267 case catch M:F(Args#arg.state, Payload) of
199 268 {'EXIT', Reason} ->
200 269 ?ERROR_LOG({M, F, {'EXIT', Reason}}),
201   - send(Args, 500);
  270 + {send, send(Args, 500)};
202 271 {error, Reason} ->
203 272 ?ERROR_LOG({M, F, Reason}),
204   - send(Args, 500);
  273 + {send, send(Args, 500)};
205 274 {false, ResponsePayload} ->
206   - encode_send(Args, 200, ResponsePayload, [], ID, RpcType);
207   - false -> % Soap notify !?
208   - false;
  275 + {send, encode_send(Args, 200, ResponsePayload, [], ID, RpcType)};
  276 + false -> % Soap notify
  277 + {send, send(Args, 200, RpcType)};
209 278 {true, _NewTimeout, _NewState, ResponsePayload} ->
210   - encode_send(Args, 200, ResponsePayload, [], ID, RpcType)
  279 + {send, encode_send(Args, 200, ResponsePayload, [], ID, RpcType)}
211 280 end.
212 281
213 282 handle_cookie(Cookie, CookieName, SessionValue, NewSessionValue, M, F) ->
@@ -243,85 +312,71 @@ get_expire(M, F) ->
243 312 _ -> false
244 313 end.
245 314
246   -callback_fun(M, F, Args, Payload, SessionValue, RpcType) when RpcType==soap ->
  315 +callback_fun(M, F, Args, Payload, SessionValue, soap) ->
247 316 fun() -> yaws_soap_srv:handler(Args, {M,F}, Payload, SessionValue) end;
248 317 callback_fun(M, F, Args, Payload, SessionValue, _RpcType) ->
249 318 fun() -> M:F(Args#arg.state, Payload, SessionValue) end.
250 319
251   -
252 320 %%% XXX compatibility with XMLRPC handlers
253 321 %%% XXX - potential bug here?
254 322 encode_send(Args, StatusCode, [Payload], AddOn, ID, RpcType) ->
255 323 encode_send(Args, StatusCode, Payload, AddOn, ID, RpcType);
256 324
257   -encode_send(_Args, _StatusCode, Payload, _AddOn, ID, RpcType) ->
  325 +encode_send(Args, StatusCode, Payload, _AddOn, ID, RpcType) ->
258 326 ?Debug("rpc response ~p ~n", [Payload]),
259 327 EncodedPayload = encode_handler_payload(Payload, ID, RpcType),
260 328 ?Debug("rpc encoded response ~p ~n", [EncodedPayload]),
261   - EncodedPayload.
  329 + send(Args, StatusCode, EncodedPayload, [], RpcType).
262 330
263   -send(Args, StatusCode) -> send(Args, StatusCode, json).
  331 +send(Args, StatusCode) ->
  332 + send(Args, StatusCode, json).
264 333
265   -send(Args, StatusCode, RpcType) -> send(Args, StatusCode, "", [], RpcType).
  334 +send(Args, StatusCode, RpcType) ->
  335 + send(Args, StatusCode, "", [], RpcType).
266 336
267 337 send(_Args, StatusCode, Payload, AddOnData, RpcType) ->
268   - A = [
269   - {status, StatusCode},
270   - content_hdr(RpcType, Payload),
271   - {header, {content_length, lists:flatlength(Payload) }}
272   - ] ++ AddOnData,
273   - A.
  338 + [{status, StatusCode},
  339 + content_hdr(RpcType, Payload),
  340 + {header, {content_length, lists:flatlength(Payload)}}] ++ AddOnData.
274 341
275 342 content_hdr(json, Payload) -> {content, "application/json", Payload};
276   -content_hdr(_, Payload) -> {content, "text/xml", Payload}.
  343 +content_hdr(_, Payload) -> {content, "application/xml", Payload}.
277 344 %% FIXME would like to add charset info here !!
278 345
279 346 encode_handler_payload({Xml,[]}, _ID, soap) ->
280 347 Xml;
281   -
282 348 encode_handler_payload(Xml, _ID, soap) ->
283 349 Xml;
284   -
285 350 encode_handler_payload({error, [ErlStruct]}, ID, RpcType) ->
286 351 encode_handler_payload({error, ErlStruct}, ID, RpcType);
287   -
288   -
289 352 encode_handler_payload({error, ErlStruct}, ID, RpcType) ->
290 353 StructStr =
291 354 case RpcType of
292   - json -> json2:encode({struct, [{result, null}, {id, ID},
293   - {error, ErlStruct}]});
  355 + json -> json2:encode({struct, [{id, ID}, {error, ErlStruct},
  356 + {"jsonrpc", "2.0"}]});
294 357 haxe -> [$h, $x, $r | haxe:encode({exception, ErlStruct})]
295 358 end,
296 359 StructStr;
297   -
298 360 encode_handler_payload({response, [ErlStruct]}, ID, RpcType) ->
299 361 encode_handler_payload({response, ErlStruct}, ID, RpcType);
300   -
301   -
302 362 encode_handler_payload({response, ErlStruct}, ID, RpcType) ->
303 363 StructStr =
304 364 case RpcType of
305 365 json -> json2:encode({struct, [{result, ErlStruct}, {id, ID},
306   - {error, null}]});
  366 + {"jsonrpc", "2.0"}]});
307 367 haxe -> [$h, $x, $r | haxe:encode(ErlStruct)]
308 368 end,
309 369 StructStr.
310 370
311   -
312 371 decode_handler_payload(json, JSonStr) ->
313 372 try
314 373 {ok, Obj} = json2:decode_string(JSonStr),
315   - Method = list_to_atom(jsonrpc:s(Obj, method)),
316   - {array, Args} = jsonrpc:s(Obj, params),
317   - ID = jsonrpc:s(Obj, id),
318   - {ok, {call, Method, Args}, ID}
  374 + decode_handler_payload_json(Obj)
319 375 catch
320 376 error:Err ->
321   - ?ERROR_LOG({ json_decode , JSonStr , Err }),
322   - {error, Err}
  377 + ?ERROR_LOG({json_decode, JSonStr, Err}),
  378 + {error, {-32700, Err}}
323 379 end;
324   -
325 380 decode_handler_payload(haxe, [$_, $_, $x, $= | HaxeStr]) ->
326 381 try
327 382 {done, {ok, {array, [MethodName | _]}}, Cont} = haxe:decode(HaxeStr),
@@ -338,5 +393,66 @@ decode_handler_payload(haxe, _HaxeStr) ->
338 393 decode_handler_payload(soap, Payload) ->
339 394 {ok, Payload, undefined}.
340 395
341   -
342   -
  396 +decode_handler_payload_json({struct, _}=Obj) ->
  397 + case jsonrpc:s(Obj, method) of
  398 + undefined ->
  399 + {error, -32600};
  400 + Method0 when is_list(Method0) ->
  401 + Method = case jsonrpc:s(Obj, jsonrpc) of
  402 + "2.0" ->
  403 + try
  404 + list_to_existing_atom(Method0)
  405 + catch
  406 + error:badarg ->
  407 + Method0
  408 + end;
  409 + undefined ->
  410 + list_to_atom(Method0)
  411 + end,
  412 + Args = jsonrpc:s(Obj, params),
  413 + ArgsOk = case Args of
  414 + {struct, _} -> true;
  415 + {array, _} -> true;
  416 + undefined -> true;
  417 + _ -> false
  418 + end,
  419 + case ArgsOk of
  420 + true ->
  421 + ID = jsonrpc:s(Obj, id),
  422 + CallOrNotify = case ID of
  423 + undefined ->
  424 + notification;
  425 + _ ->
  426 + call
  427 + end,
  428 + {ok, {CallOrNotify, Method, Args}, ID};
  429 + false ->
  430 + {error, -32602}
  431 + end;
  432 + _ ->
  433 + {error, -32600}
  434 + end;
  435 +decode_handler_payload_json({array, []}) ->
  436 + {error, -32600};
  437 +decode_handler_payload_json({array, Batch}) ->
  438 + [decode_handler_payload_json(Obj) || Obj <- Batch];
  439 +decode_handler_payload_json(_) ->
  440 + {error, -32600}.
  441 +
  442 +json_error(ErrCode) ->
  443 + json_error(ErrCode, null).
  444 +json_error(ErrCode, Id) ->
  445 + Err = {struct, [{"jsonrpc", "2.0"},
  446 + {"id", Id},
  447 + {"error", {struct,
  448 + [{"code", ErrCode},
  449 + {"message", json_error_message(ErrCode)}]}}]},
  450 + json2:encode(Err).
  451 +
  452 +json_error_message(-32700) -> "parse error";
  453 +json_error_message(-32600) -> "invalid request";
  454 +json_error_message(-32601) -> "method not found";
  455 +json_error_message(-32602) -> "invalid params";
  456 +json_error_message(-32603) -> "internal error";
  457 +json_error_message(Code) when Code >= -32099, Code =< -32000 -> "server error";
  458 +json_error_message(_) -> "json error".
6 test/Makefile
@@ -2,8 +2,7 @@ include support/include.mk
2 2
3 3 SUBDIRS = t1 t2 eunit
4 4
5   -all: conf fetch_ibrowse
6   - @cd ibrowse; $(MAKE)
  5 +all: conf ibrowse
7 6 @cd src; $(MAKE) all
8 7 @set -e ; \
9 8 for d in $(SUBDIRS) ; do \
@@ -30,6 +29,9 @@ test:
30 29 fi ; \
31 30 done
32 31
  32 +ibrowse: fetch_ibrowse
  33 + @cd ibrowse; $(MAKE)
  34 +
33 35 IBROWSE_URI = https://github.com/cmullaparthi/ibrowse.git
34 36 IBROWSE_DOWNLOAD_URI = https://github.com/cmullaparthi/ibrowse/tarball/master
35 37 IBROWSE_TGZ = ibrowse.tar.gz
22 test/conf/stdconf.conf
@@ -20,9 +20,9 @@ trace = false
20 20
21 21
22 22
23   -# it is possible to have yaws start additional
  23 +# it is possible to have yaws start additional
24 24 # application specific code at startup
25   -#
  25 +#
26 26 # runmod = mymodule
27 27
28 28
@@ -46,15 +46,15 @@ log_resolve_hostname = false
46 46
47 47
48 48
49   -# fail completely or not if yaws fails
  49 +# fail completely or not if yaws fails
50 50 # to bind a listen socket
51 51 fail_on_bind_err = true
52 52
53 53
54 54
55 55 # If yaws is started as root, it can, once it has opened
56   -# all relevant sockets for listening, change the uid to a
57   -# user with lower accessrights than root
  56 +# all relevant sockets for listening, change the uid to a
  57 +# user with lower accessrights than root
58 58
59 59 # username = nobody
60 60
@@ -65,7 +65,7 @@ fail_on_bind_err = true
65 65 auth_log = true
66 66
67 67
68   -# When we're running multiple yaws systems on the same
  68 +# When we're running multiple yaws systems on the same
69 69 # host, we need to give each yaws system an individual
70 70 # name. Yaws will write a number of runtime files under
71 71 # /tmp/yaws/${id}
@@ -86,7 +86,7 @@ pick_first_virthost_on_nomatch = true
86 86
87 87 # All unices are broken since it's not possible to bind to
88 88 # a privileged port (< 1024) unless uid==0
89   -# There is a contrib in jungerl which makes it possible by means
  89 +# There is a contrib in jungerl which makes it possible by means
90 90 # of an external setuid root programm called fdsrv to listen to
91 91 # to privileged port.
92 92 # If we use this feature, it requires fdsrv to be properly installed.
@@ -141,3 +141,11 @@ use_fdsrv = false
141 141 docroot = /tmp
142 142 appmods = <non_root_appmod, app_test>
143 143 </server>
  144 +
  145 +<server localhost>
  146 + port = 8005
  147 + listen = 0.0.0.0
  148 + docroot = /tmp
  149 + appmods = </, jsontest>
  150 +</server>
  151 +
2  test/t2/Makefile
@@ -3,7 +3,7 @@ include ../support/include.mk
3 3 .PHONY: all test debug clean
4 4
5 5 #
6   -all: conf setup app_test.beam streamtest.beam
  6 +all: conf setup app_test.beam streamtest.beam jsontest.beam
7 7 @echo "all ok"
8 8
9 9
221 test/t2/app_test.erl
@@ -20,6 +20,7 @@ start() ->
20 20 appmod_test(),
21 21 streamcontent_test(),
22 22 sendfile_get(),
  23 + json_test(),
23 24 ibrowse:stop().
24 25
25 26
@@ -266,6 +267,226 @@ streamcontent_test() ->
266 267 gen_tcp:close(Sock),
267 268 ok.
268 269
  270 +json_test() ->
  271 + io:format("json_test\n",[]),
  272 + io:format(" param array1\n", []),
  273 + ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
  274 + {"method", "subtract"},
  275 + {"params", {array, [42, 23]}},
  276 + {"id", 1}]},
  277 + {struct, [{"jsonrpc", "2.0"},
  278 + {"result", 19},
  279 + {"id", 1}]}),
  280 + io:format(" param array2\n", []),
  281 + ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
  282 + {"method", "subtract"},
  283 + {"params", {array, [23, 42]}},
  284 + {"id", 2}]},
  285 + {struct, [{"jsonrpc", "2.0"},
  286 + {"result", -19},
  287 + {"id", 2}]}),
  288 + io:format(" param obj1\n", []),
  289 + ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
  290 + {"method", "subtract"},
  291 + {"params", {struct, [{"subtrahend", 23},
  292 + {"minuend", 42}]}},
  293 + {"id", 3}]},
  294 + {struct, [{"jsonrpc", "2.0"},
  295 + {"result", 19},
  296 + {"id", 3}]}),
  297 + io:format(" param obj2\n", []),
  298 + ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
  299 + {"method", "subtract"},
  300 + {"params", {struct, [{"minuend", 42},
  301 + {"subtrahend", 23}]}},
  302 + {"id", 4}]},
  303 + {struct, [{"jsonrpc", "2.0"},
  304 + {"result", 19},
  305 + {"id", 4}]}),
  306 + io:format(" notif1\n", []),
  307 + ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
  308 + {"method", "update"},
  309 + {"params", {array, [1,2,3,4,5]}}]},
  310 + notification),
  311 + io:format(" notif2\n", []),
  312 + ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
  313 + {"method", "foobar"}]},
  314 + notification),
  315 + io:format(" missing method\n", []),
  316 + ?line ok = do_json({struct, [{"jsonrpc", "2.0"},
  317 + {"method", "foobar"},
  318 + {"id", "1"}]},
  319 + {struct, [{"jsonrpc", "2.0"},
  320 + {"id", "1"},
  321 + {"error", {struct,
  322 + [{"code", -32601},
  323 + {"message", "method not found"}]}
  324 + }]}),
  325 + io:format(" invalid json\n", []),
  326 + InvalidJson = "{\"jsonrpc\": \"2.0\", \"method\": \"foobar,"
  327 + "\"params\": \"bar\", \"baz]",
  328 + ?line ok = do_json(InvalidJson,
  329 + {struct, [{"jsonrpc", "2.0"},
  330 + {"id", null},
  331 + {"error", {struct,
  332 + [{"code", -32700},
  333 + {"message", "parse error"}]}
  334 + }]},
  335 + no_encode),
  336 + io:format(" invalid req1\n", []),
  337 + InvalidReq1 = "{\"jsonrpc\": \"2.0\", \"method\": 1, \"params\": \"bar\"}",
  338 + ?line ok = do_json(InvalidReq1,
  339 + {struct, [{"jsonrpc", "2.0"},
  340 + {"id", null},
  341 + {"error", {struct,
  342 + [{"code", -32600},
  343 + {"message", "invalid request"}]}
  344 + }]},
  345 + no_encode),
  346 + io:format(" invalid params\n", []),
  347 + InvalidReq2 = "{\"jsonrpc\": \"2.0\", \"method\": \"x\","
  348 + "\"params\": \"bar\"}",
  349 + ?line ok = do_json(InvalidReq2,
  350 + {struct, [{"jsonrpc", "2.0"},
  351 + {"id", null},
  352 + {"error", {struct,
  353 + [{"code", -32602},
  354 + {"message", "invalid params"}]}
  355 + }]},
  356 + no_encode),
  357 + io:format(" invalid batch json\n", []),
  358 + InvalidJsonBatch = "[ {\"jsonrpc\": \"2.0\", \"method\": \"sum\","
  359 + "\"params\": [1,2,4],\"id\": \"1\"},{\"jsonrpc\": \"2.0\", \"method\" ]",
  360 + ?line ok = do_json(InvalidJsonBatch,
  361 + {struct, [{"jsonrpc", "2.0"},
  362 + {"id", null},
  363 + {"error", {struct,
  364 + [{"code", -32700},
  365 + {"message", "parse error"}]}
  366 + }]},
  367 + no_encode),
  368 + io:format(" empty batch\n", []),
  369 + EmptyBatch = "[]",
  370 + ?line ok = do_json(EmptyBatch,
  371 + {struct, [{"jsonrpc", "2.0"},
  372 + {"id", null},
  373 + {"error", {struct,
  374 + [{"code", -32600},
  375 + {"message", "invalid request"}]}
  376 + }]},
  377 + no_encode),
  378 + io:format(" invalid batch1\n", []),
  379 + BogusBatch1 = "[1]",
  380 + ?line ok = do_json(BogusBatch1,
  381 + {array,
  382 + [{struct, [{"jsonrpc", "2.0"},
  383 + {"id", null},
  384 + {"error", {struct,
  385 + [{"code", -32600},
  386 + {"message", "invalid request"}]}
  387 + }]}]},
  388 + no_encode),
  389 + io:format(" invalid batch2\n", []),
  390 + BogusBatch2 = "[1,2,3]",
  391 + ?line ok = do_json(BogusBatch2,
  392 + {array,
  393 + [{struct, [{"jsonrpc", "2.0"},
  394 + {"id", null},
  395 + {"error", {struct,
  396 + [{"code", -32600},
  397 + {"message", "invalid request"}]}
  398 + }]},
  399 + {struct, [{"jsonrpc", "2.0"},
  400 + {"id", null},
  401 + {"error", {struct,
  402 + [{"code", -32600},
  403 + {"message", "invalid request"}]}
  404 + }]},
  405 + {struct, [{"jsonrpc", "2.0"},
  406 + {"id", null},
  407 + {"error", {struct,
  408 + [{"code", -32600},
  409 + {"message", "invalid request"}]}
  410 + }]}]},
  411 + no_encode),
  412 + io:format(" mixed batch\n", []),
  413 + MixedBatch = "
  414 + [{\"jsonrpc\":\"2.0\",\"method\":\"sum\",\"params\": [1,2,4], \"id\": \"1\"},
  415 + {\"jsonrpc\":\"2.0\",\"method\":\"notify_hello\", \"params\": [7]},
  416 + {\"jsonrpc\":\"2.0\",\"method\":\"subtract\",\"params\":[42,23],
  417 + \"id\":\"2\"},
  418 + {\"foo\": \"boo\"},
  419 + {\"jsonrpc\":\"2.0\",\"method\":\"foo.get\",
  420 + \"params\":{\"name\": \"myself\"}, \"id\": \"5\"},
  421 + {\"jsonrpc\": \"2.0\", \"method\": \"get_data\", \"id\": \"9\"}]",
  422 + ?line ok = do_json(MixedBatch,
  423 + {array,
  424 + [{struct,[{"jsonrpc","2.0"},
  425 + {"result",7},
  426 + {"id","1"}]},
  427 + {struct,[{"jsonrpc","2.0"},
  428 + {"result",19},{"id","2"}]},
  429 + {struct,[{"jsonrpc","2.0"},
  430 + {"error",
  431 + {struct,[{"code",-32600},
  432 + {"message","invalid request"}]}},
  433 + {"id",null}]},
  434 + {struct,[{"jsonrpc","2.0"},
  435 + {"error",
  436 + {struct,[{"code",-32601},
  437 + {"message","method not found"}]}},
  438 + {"id","5"}]},
  439 + {struct,[{"jsonrpc","2.0"},
  440 + {"result",{array,["hello",5]}},
  441 + {"id","9"}]}]},
  442 + no_encode),
  443 + io:format(" all-notification batch\n", []),
  444 + NotifBatch = "[
  445 + {\"jsonrpc\": \"2.0\", \"method\": \"notify_sum\", \"params\": [1,2,4]},
  446 + {\"jsonrpc\": \"2.0\", \"method\": \"notify_hello\", \"params\": [7]}]",
  447 + ?line ok = do_json(NotifBatch, notification, no_encode),
  448 + ok.
  449 +
  450 +do_json(Req, Expected) ->
  451 + do_json(Req, Expected, encode).
  452 +do_json(Req, notification, NeedEncode) ->
  453 + ?line {ok, "200", Headers, Body} = json_send(Req, NeedEncode),
  454 + ?line "application/json" = proplists:get_value("Content-Type", Headers),
  455 + ?line [] = Body,
  456 + ok;
  457 +do_json(Req, {struct, _}=Expected, NeedEncode) ->
  458 + ?line {ok, "200", Headers, Body} = json_send(Req, NeedEncode),
  459 + ?line "application/json" = proplists:get_value("Content-Type", Headers),
  460 + check_json(Expected, Body, true);
  461 +do_json(Req, {array, Array}, NeedEncode) ->
  462 + ?line {ok, "200", Headers, Body} = json_send(Req, NeedEncode),
  463 + ?line "application/json" = proplists:get_value("Content-Type", Headers),
  464 + ?line {ok, {array, GotArray}} = json2:decode_string(Body),
  465 + lists:map(fun({Obj, Got}) ->
  466 + ?line ok = check_json(Obj, Got, false)
  467 + end, lists:zip(Array, GotArray)),
  468 + ok.
  469 +
  470 +check_json({struct, _}=Exp, Body, true) ->
  471 + ?line {ok, DecodedBody} = json2:decode_string(Body),
  472 + check_json(Exp, DecodedBody);
  473 +check_json({struct, _}=Exp, Body, false) ->
  474 + check_json(Exp, Body).
  475 +check_json({struct, Members}, DecodedBody) ->
  476 + lists:foreach(fun({Key, Val}) ->
  477 + ?line Val = jsonrpc:s(DecodedBody, Key)
  478 + end, Members),
  479 + ok.
  480 +
  481 +json_send(Req) ->
  482 + json_send(Req, encode).
  483 +json_send(Req, encode) ->
  484 + json_send(json2:encode(Req), no_encode);
  485 +json_send(Req, no_encode) ->
  486 + Uri = "http://localhost:8005/jsontest",
  487 + ReqHdrs = [{content_type, "application/json"}],
  488 + ibrowse:send_req(Uri, ReqHdrs, post, Req).
  489 +
269 490 recv_hdrs(Sock) ->
270 491 recv_hdrs(Sock, 0).
271 492 recv_hdrs(Sock, Len) ->
28 test/t2/jsontest.erl
... ... @@ -0,0 +1,28 @@
  1 +-module(jsontest).
  2 +-export([out/1, handler/3]).
  3 +
  4 +out(Arg) ->
  5 + yaws_rpc:handler_session(Arg, {?MODULE, handler}).
  6 +
  7 +handler(_State, {call, subtract, Params}, _Session) ->
  8 + {Minuend, Subtrahend} = case Params of
  9 + {array, [M, S]} ->
  10 + {M, S};
  11 + Obj ->
  12 + {jsonrpc:s(Obj, "minuend"),
  13 + jsonrpc:s(Obj, "subtrahend")}
  14 + end,
  15 + {true, undefined, undefined, {response, Minuend - Subtrahend}};
  16 +handler(_State, {notification, update, {array, [1,2,3,4,5]}}, _Session) ->
  17 + false;
  18 +handler(_State, {notification, foobar, undefined}, _Session) ->
  19 + false;
  20 +handler(_State, {call, sum, {array, Params}}, _Session) ->
  21 + {true, undefined, undefined, {response, lists:sum(Params)}};
  22 +handler(_State, {notification, "notify_hello", {array, [7]}}, _Session) ->
  23 + false;
  24 +handler(_State, {call, get_data, undefined}, _Session) ->
  25 + {true, undefined, undefined, {response, {array, ["hello", 5]}}}.
  26 +
  27 +
  28 +
180 www/json_intro.yaws
@@ -19,67 +19,76 @@ ss(A, File) ->
19 19 {ok, B} = file:read_file(
20 20 filename:join([A#arg.docroot, File])),
21 21 box(binary_to_list(B)).
22   -
23 22
24 23
25   -out(A) ->
  24 +
  25 +out(A) ->
26 26 [{ssi, "TAB.inc", "%%",[{"json_intro", "choosen"}]},
27 27 {ehtml,
28 28 {'div', [{id, "entry"}],
29 29
30 30 [{h1, [], "AJAX through JSON RPC"},
31   -
  31 +
32 32 {p, [],
33   - {i, [],
  33 + {i, [],
34 34 ["Note: this documentation used to refer to the module "
35   - "'yaws_jsonrpc'. This module has been depracated in favor "
36   - "of 'yaws_rpc', which handles both JSON RPC, haXe and SOAP "
37   - "remoting. All references to 'yaws_jsonrpc' on this page "
38   - "were therefore changed to 'yaws_rpc'. For more specific "
39   - "information about SOAP, refer to ",
  35 + "'yaws_jsonrpc', but that module was deprecated in favor of "
  36 + "'yaws_rpc', which handles JSON RPC, haXe and SOAP remoting. "
  37 + "For more specific information about SOAP, refer to ",
40 38 {a, [{href, "/soap_intro.yaws"}], "the SOAP page."}]}},
41 39 {p, [],
42   - ["The Yaws Json binding is a way to have Javascript code in the "
43   - "browser evaluate a remote procedure call in the Yaws server."
  40 + ["The Yaws JSON-RPC binding is a way to have JavaScript code in the "
  41 + "browser evaluate a remote procedure call (RPC) in the Yaws server. "
44 42 "JSON itself as described at ",
45 43 {a, [{href, "http://www.json.org/"}], "http://www.json.org/ "},
46 44 "is basically a simple marshaling format which can be used "
47   - " from a variety of different programming languages, in particular "
48   - " it completely straightforward to implement in Javascript."]},
  45 + "from a variety of different programming languages, and "
  46 + "naturally it's completely straightforward to implement "
  47 + "in JavaScript itself. JSON-RPC version 2.0, the version Yaws "
  48 + "supports, is described here:"]},
  49 + {p, [],
  50 + [{a, [{href,
  51 + "http://groups.google.com/group/json-rpc/web/json-rpc-2-0"}],
  52 + "http://groups.google.com/group/json-rpc/web/json-rpc-2-0"}]},
49 53 {p, [],
50   - "The yaws JSON implementation consist of Javascript client and a "
51   - " server side library which must be explicitly invoked by Erlang "
52   - "code in a .yaws page."},
  54 + "The Yaws JSON-RPC implementation consist of JavaScript clients and a "
  55 + "server side library that must be explicitly invoked by Erlang "
  56 + "code in a .yaws page, appmod, etc."},
53 57
54 58 {p,[],
55 59 "It is not particularly easy to show and explain an AJAX setup "
56   - "through JSON RPC, but here is an attempt:"
  60 + "through JSON-RPC, but here is an attempt:"
57 61 },
58   - {p,[],
  62 + {p,[],
59 63 "First we have an HTML page which:"},
60 64 {ol, [],
61 65 [
62   - {li,[],{p,[], "Includes the client side of the JSON library."
63   - " The library is included in the yaws distribution "
64   - " and it is found under \"www/jsolait/jsolait.js\" ."}},
65   - {li,[],{p,[],"Second the HTML code defines the name of a method, "
66   - "i.e. the name of a server side function which shall be "
67   - " called by the client side Javascript code"}},
68   - {li,[],{p,[],"Finally the HTML code defines a FORM which is "
  66 + {li,[],{p,[],
  67 + ["Includes the client side of the JSON library. "
  68 + "The library is included in the Yaws distribution "
  69 + "and it is found under ",
  70 + {a,
  71 + [{href,
  72 + "https://github.com/klacke/yaws/blob/master/www/jsolait/jsolait.js"}],
  73 + "\"www/jsolait/jsolait.js\""}, "."]}},
  74 + {li,[],{p,[],"Second, the HTML code defines the name of a method, "
  75 + "i.e. the name of a server-side function that shall be "
  76 + "called by the client side JavaScript code."}},
  77 + {li,[],{p,[],"Finally the HTML code defines a FORM that's "
69 78 "used to invoke the RPC. This is just a really simple "
70   - "example, really any Javascript code can invoke any RPC in "
71   - "more interesting scenarios than submitting a form"}}]},
72   -
73   - {p, [], "The HTML code looks like "},
  79 + "example, really any JavaScript code can invoke any RPC in "
  80 + "more interesting scenarios than submitting a form."}}]},
  81 +
  82 + {p, [], "The HTML code appears as shown below:"},
74 83 ss(A,"json_sample.html"),
75 84 {p, [], ["This HTML code resides in file ",
76 85 {a,[{href, "json_sample.html"}], "json_sample.html"},
77   - " and it is the HTML code that is the AJAX GUI !!!"]},
78   - {p, [], "Following that we need to take a look at the page "
79   - "json_sample.yaws which is the \"serviceURL\" according to "
80   - "the Javascript code. This code defines the function to be "
81   - "called. Remember that the Javascript code defined one method, "
82   - "called \"test1\", this information will be passed to the "
  86 + " and it is the HTML code that is the AJAX GUI."]},
  87 + {p, [], "Following that we need to take a look at json_sample.yaws "
  88 + " (shown below), which is the \"serviceURL\" according to "
  89 + "the JavaScript code. This code defines the function to be "
  90 + "called. Remember that the JavaScript code defined one method, "
  91 + "called \"test1\"; this information will be passed to the "
83 92 "serviceURL. The code looks like:"},
84 93 ss(A, "json_sample.yaws"),
85 94
@@ -91,15 +100,20 @@ out(A) ->
91 100 {li,[],
92 101 {pre,[],"counter([{ip, IP}] = _State, {call, test1, Value} = _Request, Session)"}}]},
93 102
94   - {p,[],
95   - "The first line tells Yaws to forward all JSON-RPC methods to the "
96   - " \"counter\" function in the \"sample_mod\" module. "
97   - "The second line says, "
98   - " basically, - this is the counter function that will be called when "
99   - " the client invokes a method called 'test1'. We would duplicate "
100   - " this line with a different name than 'test1' for each RPC function "
101   - "we wish to implement."},
102   -
  103 + {p,[],
  104 + ["The first line tells Yaws to forward all JSON-RPC methods to the "
  105 + " \"counter\" function in the \"sample_mod\" module. "
  106 + "The second line is the head of the counter function that will be "
  107 + "called when the client invokes a method called 'test1'. We would "
  108 + "duplicate this line with a different name than 'test1' for each RPC "
  109 + "function we wish to implement. Note that the first atom in the "
  110 + "request tuple will either be 'call' or 'notification' to indicate "
  111 + "the type of request. As per the ",
  112 + {a,[{href,"http://groups.google.com/group/json-rpc/web/json-rpc-2-0"}],
  113 + "JSON-RPC 2.0 specification"},
  114 + ", a 'call' is a regular request-reply while a 'notification' is a "
  115 + "one-way message that does not have a corresponding reply."]},
  116 +
103 117 {p,[],"On the client side we have"},
104 118
105 119 box("
@@ -108,50 +122,45 @@ var jsonrpc = imprt(\"jsonrpc\");
108 122 var service = new jsonrpc.ServiceProxy(serviceURL, methods);
109 123 "),
110 124
111   - {p,[],"Which registers the Yaws page with the JSON-RPC handler and "
  125 + {p,[],"This registers the Yaws page with the JSON-RPC handler and "
112 126 "gives it a list of methods that the Yaws page can satisfy. "
113   - "In this case, it is only the method called 'test1'."},
114   -
115   - {p, [],
116   -"When we wish to return structured data - we simply let "
117   -"the user defined RPC function return JSON structures such as "},
  127 + "In this case, the only method called 'test1'."},
  128 +
  129 + {p, [],
  130 +"When we wish to return structured data, we simply let "
  131 +"the user-defined RPC function return JSON structures such as "},
118 132
119   -box(" {struct, [{field1, \"foo\"}, {field2, \"bar\"}]} "),
  133 +box("{struct, [{field1, \"foo\"}, {field2, \"bar\"}]} "),
120 134 {p, [], " for a structure and "},
121 135 box("{array, [\"foo\", \"bar\"]}"),
122   -{p, [],"for an array. We can nest arrays and structs in each other "},
123   -
124   -
125   -
126   -
  136 +{p, [],"for an array. We can nest arrays and structs in each other."},
127 137
128 138 {p, [],
129 139 "Finally, we must stress that this example is extremely simple. "
130 140 "In order to build a proper AJAX application in Yaws, a lot of "
131 141 "client side work is required, all Yaws provides is the basic "
132   - "mechanism whereby the client side Javascript code, can RPC the "
  142 + "mechanism whereby the client side JavaScript code can RPC the "
133 143 "web server for data which can be subsequently used to populate "
134 144 "the DOM. Also required to build a good AJAX application is "
135   - "good knowledge on how the DOM in the browser works"},
  145 + "good knowledge of how the DOM in the browser works"},
136 146
137 147 {p, [],
138   - [{b,[],"Update:"},
139   - "The yaws_rpc:handler will now also call: M:F(cookie_expire) which is "
140   - "expected to return a proper Cookie expire string. This makes it possible "
141   - "to setup the Cookie lifetime. If this calback function is non-existant, "
142   - "the default behaviour is to not set a cookie expiration time, i.e it will "
143   - "live for this session only."]},
  148 + ["The yaws_rpc:handler will also call: M:F(cookie_expire) "
  149 + "which is expected to return a proper Cookie expire string. This "
  150 + "makes it possible to setup the Cookie lifetime. If this callback "
  151 + "function is non-existent, the default behaviour is to not set a "
  152 + "cookie expiration time, i.e., it will live for this session only."]},
144 153
145 154 {h3, [], "One more example "},
146 155
147 156 {p, [],
148 157 ["Here is yet another example, stolen from ",
149   - {a,
150   - [{href,"http://blog.tornkvist.org/blog.yaws?id=1179347231289901#"}],
151   - "Tobbes blog"}
  158 + {a,
  159 + [{href,"http://www.redhoterlang.com/entry/ac061493b201e3d1b4490cdc3f911068"}],
  160 + "Tobbe's blog."}
152 161 ]},
153   - {h4, [], "Setup the DOM"},
154   - {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."},
  162 + {h4, [], "Setup the DOM"},
  163 + {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."},
155 164 box("
156 165
157 166 <html>
@@ -204,7 +213,7 @@ out(A) ->
204 213 L = yaws_api:parse_query(A),
205 214 dispatch(lkup(\"op\", L, false), A, L).
206 215
207   -dispatch(\"ex1\", A, L) ->
  216 +dispatch(\"ex1\", A, L) ->
208 217 ex1(A, L).
209 218
210 219 ex1(_A, L) ->
@@ -222,7 +231,7 @@ two() -> obj(\"two\").
222 231 three() -> obj(\"three\").
223 232
224 233 obj(M) ->
225   - obj(M, \"r\").
  234 + obj(M, \"r\").
226 235
227 236 %%%
228 237 %%% How ::= \"r\" | \"a\" , r=replace, a=append
@@ -235,8 +244,8 @@ obj(M, How) ->
235 244 {\"what\", C ++\" \"++M++\" content\"}]}].
236 245
237 246 return_json(Json) ->
238   - {content,
239   - \"application/json; charset=iso-8859-1\",
  247 + {content,
  248 + \"application/json; charset=iso-8859-1\",
240 249 Json}.
241 250
242 251 now2str() ->
@@ -254,27 +263,18 @@ lkup(Key, List, Def) ->
254 263
255 264 ") ,
256 265 {h2, [], "The json library"},
257   - {p, [], "The Yaws JSON library contains 3 simple functions, "
258   - " One to encode and two for decoding. See source code json2.erl "
259   - " for detailed instructions on usage "}
260   -
261   -
262   -
263   -
  266 + {p, [],
  267 + ["The Yaws JSON library contains 3 simple functions, "
  268 + " one for encoding and two for decoding. See source code ",
  269 + {a,
  270 + [{href,
  271 + "https://github.com/klacke/yaws/blob/master/src/json2.erl"}],
  272 + "json2.erl"},
  273 + " for detailed instructions on usage."]}
264 274
265 275 ]}},
266 276
267 277 {ssi, "END2",[],[]}
268 278 ].
269 279
270   -
271   -
272   -
273 280 </erl>
274   -
275   -
276   -
277   -
278   -
279   -
280   -

0 comments on commit 15454bc

Please sign in to comment.
Something went wrong with that request. Please try again.