AJAX through JSON RPC

Note: this documentation used to refer to the module '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 the SOAP page.

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 http://www.json.org/ is basically a simple marshaling format which can be used 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:

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

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.

It is not particularly easy to show and explain an AJAX setup through JSON-RPC, but here is an attempt:

First we have an HTML page which:

  1. 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".

  2. 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.

  3. 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.

The HTML code appears as shown below:

<html>
  <head>
    <title>Testing html-json library</title>
  </head>
  <script src="jsolait/jsolait.js"></script>
  <script>

var serviceURL = "json_sample.yaws";
var methods = [ "test1", "errortest" ];

var jsonrpc = imprt("jsonrpc");
var service = new jsonrpc.ServiceProxy(serviceURL, methods);

function test() {
    try {
     foo = document.getElementById('foo').value;
     bar = document.getElementById('bar').value;
     document.getElementById('result').innerHTML =
       "<PRE>" + service.test1(foo, bar) + "</PRE>";
     } catch(e) {
        alert(e);
     }
     return false;
}

function errortest() {
    try {
     document.getElementById('failure').innerHTML =
       "<PRE>" + service.errortest() + "</PRE>";
     } catch(e) {
     document.getElementById('failure').innerHTML =
        "<PRE>" + e + "</PRE>";
     }
     return false;
}

  </script>
  <body>
    <form action="" method="post" onSubmit="return test()">
      <div id="result">
      </div>
      <p>
        First Argument:  <input id="foo" />
      </p>
      <p>
        Second Argument: <input id="bar"/>
      </p>
      <p>
        <input type="submit" value="Do JSON-RPC call"/>
      </p>
    </form>
    <form action="" method="post" onSubmit="return errortest()">
      <p>
        <input type="submit" value="Do JSON-RPC call expected to fail"/>
      </p>
      <div id="failure">
      </div>
    </form>
  </body>
</html>

This HTML code resides in file json_sample.html and it is the HTML code that is the AJAX GUI.

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:

<erl module=sample_mod>
-compile(export_all).

out(A) ->
    Peer = case yaws_api:get_sslsocket(A#arg.clisock) of
               {ok, SslSocket} ->
                   ssl:peername(SslSocket);
               _ ->
                   inet:peername(A#arg.clisock)
           end,

    {ok,{IP,_}} = Peer,
    A2=A#arg{state = [{ip, IP}]},
    yaws_rpc:handler_session(A2, {?MODULE, counter}).



counter([{ip, IP}] = _State, {call, errortest, Value} = _Request, Session) ->
    io:format("Request = ~p~n", [_Request]),
    { false, { error, "Expected failure" } };

counter([{ip, IP}] = _State, {call, test1, Value} = _Request, Session) ->
    io:format("Request = ~p~n", [_Request]),
    IPStr = io_lib:format("Client ip is  ~p~n" , [ IP ]),
    OldSession = io_lib:format("Request is: ~p~nOld session value "
                               "is ~p~n", [ _Request, Session ]),

    case Session of
        undefined -> % create new session
            NewSession = 0;
        10 ->        % reset session after reaching 10
            NewSession = undefined;
        N ->
            NewSession = N + 1
    end,

    NewVal = io_lib:format("New session value is ~p ~n", [ NewSession ]),
    Str = lists:flatten([IPStr,OldSession,NewVal]),
    {true, 0, NewSession, {response,  Str }}.


</erl>

The two important lines on the server side are

  1. yaws_rpc:handler_session(A2, {sample_mod, counter}).
  2. counter([{ip, IP}] = _State, {call, test1, Value} = _Request, Session)

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 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.

On the client side we have

var methods = [ "test1" ];
var jsonrpc = imprt("jsonrpc");
var service = new jsonrpc.ServiceProxy(serviceURL, methods);

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, the only method called 'test1'.

When we wish to return structured data, we simply let the user-defined RPC function return JSON structures such as

{struct, [{field1, "foo"}, {field2, "bar"}]} 

for a structure and

{array, ["foo", "bar"]}

for an array. We can nest arrays and structs in each other.

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 web server for data which can be subsequently used to populate the DOM. Also required to build a good AJAX application is good knowledge of how the DOM in the browser works

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.

One more example

Here is yet another example, stolen from Tobbe's blog.

Setup the DOM

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.


<html>
<head>
<script type="text/javascript"
           src="/jquery-1.2.3.js"></script>
</head>
<body>

<script language="javascript" type="text/javascript">

function ex1(what) {
   $.getJSON("/ex1.yaws",
            {'op': "ex1", 'what': what},
            function(x) {
              do_ex1(what, x)
            });
}

function do_ex1(what, x) {
  jQuery.each(x, doit);
}

function doit() {
  $('#'+this.who).html(this.what);
}

</script>

<button onclick="ex1('one')">Update one!</button>
<button onclick="ex1('two')">Update two!</button>
<button onclick="ex1('three')">Update three!</button>

<div id="one">This is one</div>
<div id="two">This is two</div>
<div id="three">This is three</div>

</body>
</html>

The erlang server side

This is the code that needs to be installed and execute on the server side. It nicely illustrates how to return JSON structs to the client.

-module(ex1).
-export([out/1]).

out(A) ->
    L = yaws_api:parse_query(A),
    dispatch(lkup("op", L, false), A, L).

dispatch("ex1", A, L) ->
    ex1(A, L).

ex1(_A, L) ->
    J = json2:encode(array(what(lkup("what", L, false)))),
    return_json(J).

what("one")   -> one();
what("two")   -> one() ++ two();
what("three") -> one() ++ two() ++ three().

array(L) -> {array, L}.

one()   -> obj("one").
two()   -> obj("two").
three() -> obj("three").

obj(M) ->
    obj(M, "r").

%%%
%%% How ::= "r" | "a"  , r=replace, a=append
%%%
obj(M, How) ->
    C = now2str(),
    [{struct,
      [{"who", M},
       {"how", How},
       {"what", C ++" "++M++" content"}]}].

return_json(Json) ->
    {content,
    "application/json; charset=iso-8859-1",
    Json}.

now2str() ->
    {A,B,C} = erlang:now(),
    i2l(A)++"-"++i2l(B)++"-"++i2l(C).

i2l(I) when is_integer(I) -> integer_to_list(I);
i2l(L) when is_list(L)    -> L.

lkup(Key, List, Def) ->
    case lists:keysearch(Key, 1, List) of
    {value,{_,Value}} -> Value;
    _                 -> Def
    end.

The json library

The Yaws JSON library contains 3 simple functions, one for encoding and two for decoding. See source code json2.erl for detailed instructions on usage.

Valid XHTML 1.0!