From: Greg Burri Date: Sun, 20 Apr 2008 21:07:08 +0000 (+0000) Subject: MOD Avancement sur le passage à JSON X-Git-Tag: 1.0.0^2~148 X-Git-Url: http://git.euphorik.ch/?p=euphorik.git;a=commitdiff_plain;h=e04de9d41e7955b0092fc33b8619b4627af6b3f3 MOD Avancement sur le passage à JSON --- diff --git a/doc/TODO.txt b/doc/TODO.txt index 2c7a753..a9000be 100755 --- a/doc/TODO.txt +++ b/doc/TODO.txt @@ -6,15 +6,15 @@ [ok] Supprimer l'envoie de la description des conversations lors du refresh ainsi que modifié la manière de créer les conversations (maj des diagrammes de séquence) [ok] Navigation vers les pages précédentes [ok] Lien vers une conversation dans les messages sous cette forme {5F}. Le clic dessus ouvre la conversation. Egalement un bouton sur chaque conversation pour insérer son lien dans le message en cours de rédaction - * Mettre à jour la CSS de chaque skin + [ok] Mettre à jour la CSS de chaque skin * Flush le profil lors du déchargement de la page * Envoyer les infos des conversations avec l'attente d'events -* Problème de rafraichissement des couleurs des messages auquels on répond -* Changer les noms des css : Light -> Cold, Old -> Classic +* Problème de rafraichissement des couleurs des messages auquels on répond * Remplacer l'XML par du JSON. gain en simplicité et en temps d'execution. * Tester sur un prototype : l'authentification * Si concluant passage complet à JSON - * Les id ne sont plus passés en base 36 + * Les id ne sont plus passés en base 36 +* Changer les noms des css : Light -> Cold, Old -> Classic * Utiliser une listbox pour la liste des css * Avoir un thème de discussion affiché en haut des messages genre appellé "troll de la semaine : linux sera-t-il desktop ready en 2008?" * Faire une page faq et raconter n'importe quoi (entre autre la limitation avec firefox) "pourquoi ce site à des couleurs qui ne veulent rien dire ?" @@ -186,6 +186,9 @@ ok : Implémenté * La balise pour mettre des spoilers [2] Pouvoir cacher les dates [3] Gestion de l'historique (calendrier) +[4] Pouvoir voir le profile des personnes. + * Voir leurs derniers messages + * Une page de recherche de personne [4] Pouvoir choisir une couleur pour son pseudo [4] Créer un gamebot pour lancer des jeux. Par exemple un jeu d'énigmes [4] Utiliser XMLRPC ou SOAP ou JSON pour la communication client -> serveur (boah, faut pas déconner :)) diff --git a/index.html b/index.html index 2740552..b8a06b2 100755 --- a/index.html +++ b/index.html @@ -11,6 +11,7 @@ + diff --git a/js/euphorik.js b/js/euphorik.js index c880e8f..bd0cd3a 100755 --- a/js/euphorik.js +++ b/js/euphorik.js @@ -154,6 +154,15 @@ Util.prototype.xmlVersAction = function(xml) //return {action: this.to_utf8(this.serializeXML(xml /*, "UTF-8"*/))} return {action: this.serializeXML(xml)} } + +/** + * Utilisé pour l'envoie de donnée avec la méthode ajax de jQuery. + */ +Util.prototype.jsonVersAction = function(json) +{ + // FIXME : ne plus encapsuler json dans de l'xml (problème avec yaws) + return {action: "" + JSON.stringify(json) + "" } +} Util.prototype.md5 = function(chaine) { @@ -538,6 +547,15 @@ Client.prototype.getXMLlogin = function(login, password) return XMLDocument } +Client.prototype.getJSONLogin = function(login, password) +{ + return { + "action" : "authentification", + "login" : login, + "password" : password + } +} + Client.prototype.getXMLloginCookie = function() { var XMLDocument = this.util.creerDocumentXMLAction() @@ -681,7 +699,8 @@ Client.prototype.connexionCookie = function() Client.prototype.connexionLogin = function(login, password) { - return this.connexion(this.util.xmlVersAction(this.getXMLlogin(login, password))) + // return this.connexion(this.util.xmlVersAction(this.getXMLlogin(login, password))) + return this.connexion(this.util.jsonVersAction(this.getJSONLogin(login, password))) } Client.prototype.enregistrement = function(login, password) @@ -711,7 +730,7 @@ Client.prototype.connexion = function(action) async: false, type: "POST", url: "request", - dataType: "xml", + dataType: "json", data: action, success: function(data) diff --git a/js/json2.js b/js/json2.js new file mode 100644 index 0000000..25ff1ec --- /dev/null +++ b/js/json2.js @@ -0,0 +1,461 @@ +/* + http://www.JSON.org/json2.js + 2008-03-24 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + This file creates a global JSON object containing three methods: stringify, + parse, and quote. + + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects without a toJSON + method. It can be a function or an array. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t'), it contains the + characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method will + be passed the key associated with the value, and this will be bound + to the object holding the key. + + This is the toJSON method added to Dates: + + function toJSON(key) { + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + } + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If no replacer parameter is provided, then a default replacer + will be used: + + function replacer(key, value) { + return Object.hasOwnProperty.call(this, key) ? + value : undefined; + } + + The default replacer is passed the key and value for each item in + the structure. It excludes inherited members. + + If the replacer parameter is an array, then it will be used to + select the members to be serialized. It filters the results such + that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representaions, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the value + that is filled with line breaks and indentation to make it easier to + read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + then indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + + JSON.quote(text) + This method wraps a string in quotes, escaping some characters + as needed. + + + This is a reference implementation. You are free to copy, modify, or + redistribute. + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD THIRD PARTY + CODE INTO YOUR PAGES. +*/ + +/*jslint regexp: true, forin: true, evil: true */ + +/*global JSON */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, floor, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, length, + parse, propertyIsEnumerable, prototype, push, quote, replace, stringify, + test, toJSON, toString +*/ + +if (!this.JSON) { + +// Create a JSON object only if one does not already exist. We create the +// object in a closure to avoid global variables. + + JSON = function () { + + function f(n) { // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + Date.prototype.toJSON = function () { + +// Eventually, this method will be based on the date.toISOString method. + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + + var escapeable = /["\\\x00-\x1f\x7f-\x9f]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + return escapeable.test(string) ? + '"' + string.replace(escapeable, function (a) { + var c = meta[a]; + if (typeof c === 'string') { + return c; + } + c = a.charCodeAt(); + return '\\u00' + Math.floor(c / 16).toString(16) + + (c % 16).toString(16); + }) + '"' : + '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// If the object has a dontEnum length property, we'll treat it as an array. + + if (typeof value.length === 'number' && + !(value.propertyIsEnumerable('length'))) { + +// The object is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + partial.join(',\n' + gap) + + '\n' + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + k = rep[i]; + if (typeof k === 'string') { + v = str(k, value, rep); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + v = str(k, value, rep); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : + gap ? '{\n' + gap + partial.join(',\n' + gap) + + '\n' + mind + '}' : + '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + + +// Return the JSON object containing the stringify, parse, and quote methods. + + return { + stringify: function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + if (space) { + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + } + +// If there is no replacer parameter, use the default replacer. + + if (!replacer) { + rep = function (key, value) { + if (!Object.hasOwnProperty.call(this, key)) { + return undefined; + } + return value; + }; + +// The replacer can be a function or an array. Otherwise, throw an error. + + } else if (typeof replacer === 'function' || + (typeof replacer === 'object' && + typeof replacer.length === 'number')) { + rep = replacer; + } else { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }, + + + parse: function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in three stages. In the first stage, we run the text against +// regular expressions that look for non-JSON patterns. We are especially +// concerned with '()' and 'new' because they can cause invocation, and '=' +// because it can cause mutation. But just to be safe, we want to reject all +// unexpected forms. + +// We split the first stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace all backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/.test(text.replace(/\\["\\\/bfnrtu]/g, '@'). +replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). +replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the second stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional third stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }, + + quote: quote + }; + }(); +} diff --git a/modules/Makefile b/modules/Makefile index ba4efe7..ec7ae48 100755 --- a/modules/Makefile +++ b/modules/Makefile @@ -15,7 +15,8 @@ all: $(rep_ebin)/euphorik_bd.beam \ $(rep_ebin)/euphorik_minichat.beam \ $(rep_ebin)/euphorik_minichat_conversation.beam \ $(rep_ebin)/euphorik_requests.beam \ -$(rep_ebin)/euphorik_protocole.beam +$(rep_ebin)/euphorik_protocole.beam \ +$(rep_ebin)/json.beam # Module pour la gestion de la BD, principalement la création $(rep_ebin)/euphorik_bd.beam: $(rep_erl)/euphorik_bd.erl $(rep_include)/euphorik_bd.hrl @@ -34,7 +35,11 @@ $(rep_ebin)/euphorik_requests.beam: $(rep_erl)/euphorik_requests.erl erlc $(erlc_params) # Module interpretant les messages XML du client -$(rep_ebin)/euphorik_protocole.beam: $(rep_erl)/euphorik_protocole.erl +$(rep_ebin)/euphorik_protocole.beam: $(rep_erl)/euphorik_protocole.erl $(rep_erl)/json.erl + erlc $(erlc_params) + +# Module json +$(rep_ebin)/json.beam: $(rep_erl)/json.erl erlc $(erlc_params) # Module pour la génération du captcha diff --git a/modules/erl/euphorik_protocole.erl b/modules/erl/euphorik_protocole.erl index c0090a5..7e80308 100755 --- a/modules/erl/euphorik_protocole.erl +++ b/modules/erl/euphorik_protocole.erl @@ -41,6 +41,10 @@ nouveau_user_login(Action) -> % Un utilisateur se logge. +login([{login, Login}, {password, Password}]) -> + {ok, User} = euphorik_minichat:user_by_login_password(Login, Password), + + login(Action) -> case xmerl_xpath:string("cookie", Action) of [#xmlElement{content = [#xmlText{value = Cookie}]}] -> diff --git a/modules/erl/euphorik_requests.erl b/modules/erl/euphorik_requests.erl index 0e4b046..2fb69c0 100755 --- a/modules/erl/euphorik_requests.erl +++ b/modules/erl/euphorik_requests.erl @@ -26,7 +26,7 @@ tester() -> "3FSDCH0FD4ML8WEPN2B5T" "10" "", - io:format("Messages de la premières page : ~p~n", [traiter_xml(XML)]). + io:format("Messages de la premières page : ~p~n", [traiter_donnees(XML)]). %~ traiter_xml("" %~ "4UDUSY6Z2IZNTQO484S8X" @@ -39,15 +39,30 @@ tester() -> out(A) -> %inet:setopts(A#arg.clisock, inet:getopts(A#arg.clisock, [active])), {value, {_, Contenu}} = lists:keysearch("action", 1, yaws_api:parse_post(A)), - Ret = traiter_xml(Contenu), + Ret = traiter_donnees(Contenu), {content, "text/xml", Ret}. -traiter_xml(Contenu) -> - {XML, _} = xmerl_scan:string(Contenu), - traiter_action(XML#xmlElement.attributes, XML). - - +traiter_donnees(Contenu) -> + case xmerl_scan:string(Contenu) of + {XML, _} -> + case XML of + #xmlElement{name = json, content = [#xmlText{value = J}|_]} -> + case json:decode_string(J) of + {ok, {struct, [{action, Action}| Reste]}} -> + traiter_action(Action, Reste); + _ -> + erreur + end; + _ -> + traiter_action(XML#xmlElement.attributes, XML) + end; + _ -> erreur + end. + + +traiter_action("authentification", JSON) -> + euphorik_protocole:login(JSON); % un client s'enregistre (pseudo + password) traiter_action([#xmlAttribute{value="register"}], XML) -> euphorik_protocole:nouveau_user_login(XML); diff --git a/modules/erl/json.erl b/modules/erl/json.erl new file mode 100644 index 0000000..585c05d --- /dev/null +++ b/modules/erl/json.erl @@ -0,0 +1,709 @@ +%%% Copyright (c) 2005-2006, A2Z Development USA, Inc. All Rights Reserved. +%%% +%%% The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is A2Z Development USA, Inc. +%%% All Rights Reserved. + +-module(json). +-export([encode/1, decode_string/1, decode/2]). +-export([is_obj/1, obj_new/0, obj_fetch/2, obj_find/2, obj_is_key/2]). +-export([obj_store/3, obj_from_list/1, obj_fold/3]). +-export([test/0]). +-author("Jim Larson , Robert Wai-Chi Chu "). +-vsn("1"). + +%%% JavaScript Object Notation ("JSON", http://www.json.org) is a simple +%%% data syntax meant as a lightweight alternative to other representations, +%%% such as XML. JSON is natively supported by JavaScript, but many +%%% other languages have conversion libraries available. +%%% +%%% This module translates JSON types into the following Erlang types: +%%% +%%% JSON Erlang +%%% ---- ------ +%%% number number +%%% string string +%%% array tuple +%%% object tagged proplist with string (or atom) keys +%%% true, false, null atoms 'true', 'false', and 'null' +%%% +%%% Character Sets: the external representation, and the internal +%%% representation of strings, are lists of UTF-16 code units. +%%% The encoding of supplementary characters, as well as +%%% transcoding to other schemes, such as UTF-8, can be provided +%%% by other modules. (See discussion at +%%% http://groups.yahoo.com/group/json/message/52) +%%% +%%% Numbers: Thanks to Erlang's bignums, JSON-encoded integers of any +%%% size can be parsed. Conversely, extremely large integers may +%%% be JSON-encoded. This may cause problems for interoperability +%%% with JSON parsers which can't handle arbitrary-sized integers. +%%% Erlang's floats are of fixed precision and limited range, so +%%% syntactically valid JSON floating-point numbers could silently +%%% lose precision or noisily cause an overflow. However, most +%%% other JSON libraries are likely to behave in the same way. +%%% The encoding precision defaults to 6 digits. +%%% +%%% Strings: If we represented JSON string data as Erlang binaries, +%%% we would have to choose a particular unicode format. Instead, +%%% we use lists of UTF-16 code units, which applications may then +%%% change to binaries in their application-preferred manner. +%%% +%%% Arrays: Because of the string decision above, and Erlang's +%%% lack of a distinguished string datatype, JSON arrays map +%%% to Erlang tuples. Consider utilities like tuple_fold/3 +%%% to deal with tuples in their native form. +%%% +%%% Objects: Though not explicitly stated in the JSON "spec", +%%% JSON's JavaScript heritage mandates that member names must +%%% be unique within an object. The object/tuple ambiguity is +%%% not a problem, since the atom 'json_object' is not an +%%% allowable value. Object keys may be atoms or strings on +%%% encoding but are always decoded as strings. + +%%% ENCODING + +%% Encode an erlang number, string, tuple, or object to JSON syntax, as a +%% possibly deep list of UTF-16 code units, throwing a runtime error in the +%% case of un-convertible input. +%% Note: object keys may be either strings or atoms. + +encode(true) -> "true"; +encode(false) -> "false"; +encode(null) -> "null"; +encode(I) when is_integer(I) -> integer_to_list(I); +encode(F) when is_float(F) -> io_lib:format("~g", [F]); +encode(L) when is_list(L) -> encode_string(L); +encode({}) -> "[]"; +encode({json_object, Props} = T) when is_list(Props) -> encode_object(T); +encode(T) when is_tuple(T) -> encode_array(T); +encode(Bad) -> exit({json_encode, {bad_term, Bad}}). + +%% Encode an Erlang string to JSON. +%% Accumulate strings in reverse. + +encode_string(S) -> encode_string(S, [$"]). + +encode_string([], Acc) -> lists:reverse([$" | Acc]); +encode_string([C | Cs], Acc) -> + case C of + $" -> encode_string(Cs, [$", $\\ | Acc]); + % (don't escape solidus on encode) + $\\ -> encode_string(Cs, [$\\, $\\ | Acc]); + $\b -> encode_string(Cs, [$b, $\\ | Acc]); % note missing \ + $\f -> encode_string(Cs, [$f, $\\ | Acc]); + $\n -> encode_string(Cs, [$n, $\\ | Acc]); + $\r -> encode_string(Cs, [$r, $\\ | Acc]); + $\t -> encode_string(Cs, [$t, $\\ | Acc]); + C when C >= 0, C < $\s -> + % Control characters must be unicode-encoded. + Hex = lists:flatten(io_lib:format("~4.16.0b", [C])), + encode_string(Cs, lists:reverse(Hex) ++ "u\\" ++ Acc); + C when C =< 16#FFFF -> encode_string(Cs, [C | Acc]); + _ -> exit({json_encode, {bad_char, C}}) + end. + +%% Encode an Erlang object as a JSON object, allowing string or atom keys. +%% Note that order is irrelevant in both internal and external object +%% representations. Nevertheless, the output will respect the order +%% of the input. + +encode_object({json_object, _Props} = Obj) -> + M = obj_fold(fun({Key, Value}, Acc) -> + S = case Key of + L when is_list(L) -> encode_string(L); + A when is_atom(A) -> encode_string(atom_to_list(A)); + _ -> exit({json_encode, {bad_key, Key}}) + end, + V = encode(Value), + case Acc of + [] -> [S, $:, V]; + _ -> [Acc, $,, S, $:, V] + end + end, [], Obj), + [${, M, $}]. + +%% Encode an Erlang tuple as a JSON array. +%% Order *is* significant in a JSON array! + +encode_array(T) -> + M = tuple_fold(fun(E, Acc) -> + V = encode(E), + case Acc of + [] -> V; + _ -> [Acc, $,, V] + end + end, [], T), + [$[, M, $]]. + +%% A fold function for tuples (left-to-right). +%% Folded function takes arguments (Element, Accumulator). + +tuple_fold(F, A, T) when is_tuple(T) -> + tuple_fold(F, A, T, 1, size(T)). + +tuple_fold(_F, A, _T, I, N) when I > N -> + A; +tuple_fold(F, A, T, I, N) -> + A2 = F(element(I, T), A), + tuple_fold(F, A2, T, I + 1, N). + +%%% SCANNING +%%% +%%% Scanning funs return either: +%%% {done, Result, LeftOverChars} +%%% if a complete token is recognized, or +%%% {more, Continuation} +%%% if more input is needed. +%%% Result is {ok, Term}, 'eof', or {error, Reason}. +%%% Here, the Continuation is a simple Erlang string. +%%% +%%% Currently, error handling is rather crude - errors are recognized +%%% by match failures. EOF is handled only by number scanning, where +%%% it can delimit a number, and otherwise causes a match failure. +%%% +%%% Tokens are one of the following +%%% JSON string -> erlang string +%%% JSON number -> erlang number +%%% true, false, null -> erlang atoms +%%% { } [ ] : , -> lcbrace rcbrace lsbrace rsbrace colon comma + +token([]) -> {more, []}; +token(eof) -> {done, eof, []}; + +token("true" ++ Rest) -> {done, {ok, true}, Rest}; +token("tru") -> {more, "tru"}; +token("tr") -> {more, "tr"}; +token("t") -> {more, "t"}; + +token("false" ++ Rest) -> {done, {ok, false}, Rest}; +token("fals") -> {more, "fals"}; +token("fal") -> {more, "fal"}; +token("fa") -> {more, "fa"}; +token("f") -> {more, "f"}; + +token("null" ++ Rest) -> {done, {ok, null}, Rest}; +token("nul") -> {more, "nul"}; +token("nu") -> {more, "nu"}; +token("n") -> {more, "n"}; + +token([C | Cs] = Input) -> + case C of + $\s -> token(Cs); % eat whitespace + $\t -> token(Cs); % eat whitespace + $\n -> token(Cs); % eat whitespace + $\r -> token(Cs); % eat whitespace + $" -> scan_string(Input); + $- -> scan_number(Input); + D when D >= $0, D =< $9-> scan_number(Input); + ${ -> {done, {ok, lcbrace}, Cs}; + $} -> {done, {ok, rcbrace}, Cs}; + $[ -> {done, {ok, lsbrace}, Cs}; + $] -> {done, {ok, rsbrace}, Cs}; + $: -> {done, {ok, colon}, Cs}; + $, -> {done, {ok, comma}, Cs}; + $/ -> case scan_comment(Cs) of + {more, X} -> {more, X}; + {done, _, Chars} -> token(Chars) + end; + _ -> {done, {error, {bad_char, C}}, Cs} + end. + +scan_string([$" | Cs] = Input) -> + scan_string(Cs, [], Input). + +%% Accumulate in reverse order, save original start-of-string for continuation. + +scan_string([], _, X) -> {more, X}; +scan_string(eof, _, X) -> {done, {error, missing_close_quote}, X}; +scan_string([$" | Rest], A, _) -> {done, {ok, lists:reverse(A)}, Rest}; +scan_string([$\\], _, X) -> {more, X}; +scan_string([$\\, $u, U1, U2, U3, U4 | Rest], A, X) -> + scan_string(Rest, [uni_char([U1, U2, U3, U4]) | A], X); +scan_string([$\\, $u | _], _, X) -> {more, X}; +scan_string([$\\, C | Rest], A, X) -> + scan_string(Rest, [esc_to_char(C) | A], X); +scan_string([C | Rest], A, X) -> + scan_string(Rest, [C | A], X). + +%% Given a list of hex characters, convert to the corresponding integer. + +uni_char(HexList) -> + erlang:list_to_integer(HexList, 16). + +esc_to_char($") -> $"; +esc_to_char($/) -> $/; +esc_to_char($\\) -> $\\; +esc_to_char($b) -> $\b; +esc_to_char($f) -> $\f; +esc_to_char($n) -> $\n; +esc_to_char($r) -> $\r; +esc_to_char($t) -> $\t. + +scan_number([]) -> {more, []}; +scan_number(eof) -> {done, {error, incomplete_number}, []}; +scan_number([$- | Ds] = Input) -> + case scan_number(Ds) of + {more, _Cont} -> {more, Input}; + {done, {ok, N}, CharList} -> {done, {ok, -1 * N}, CharList}; + {done, Other, Chars} -> {done, Other, Chars} + end; +scan_number([D | Ds] = Input) when D >= $0, D =< $9 -> + scan_number(Ds, D - $0, Input). + +%% Numbers don't have a terminator, so stop at the first non-digit, +%% and ask for more if we run out. + +scan_number([], _A, X) -> {more, X}; +scan_number(eof, A, _X) -> {done, {ok, A}, eof}; +scan_number([$.], _A, X) -> {more, X}; +scan_number([$., D | Ds], A, X) when D >= $0, D =< $9 -> + scan_fraction([D | Ds], A, X); +scan_number([D | Ds], A, X) when A > 0, D >= $0, D =< $9 -> + % Note that nonzero numbers can't start with "0". + scan_number(Ds, 10 * A + (D - $0), X); +scan_number([D | Ds], A, X) when D == $E; D == $e -> + scan_exponent_begin(Ds, float(A), X); +scan_number([D | _] = Ds, A, _X) when D < $0; D > $9 -> + {done, {ok, A}, Ds}. + +scan_fraction(Ds, I, X) -> scan_fraction(Ds, [], I, X). + +scan_fraction([], _Fs, _I, X) -> {more, X}; +scan_fraction(eof, Fs, I, _X) -> + R = I + list_to_float("0." ++ lists:reverse(Fs)), + {done, {ok, R}, eof}; +scan_fraction([D | Ds], Fs, I, X) when D >= $0, D =< $9 -> + scan_fraction(Ds, [D | Fs], I, X); +scan_fraction([D | Ds], Fs, I, X) when D == $E; D == $e -> + R = I + list_to_float("0." ++ lists:reverse(Fs)), + scan_exponent_begin(Ds, R, X); +scan_fraction(Rest, Fs, I, _X) -> + R = I + list_to_float("0." ++ lists:reverse(Fs)), + {done, {ok, R}, Rest}. + +scan_exponent_begin(Ds, R, X) -> + scan_exponent_begin(Ds, [], R, X). + +scan_exponent_begin([], _Es, _R, X) -> {more, X}; +scan_exponent_begin(eof, _Es, _R, X) -> {done, {error, missing_exponent}, X}; +scan_exponent_begin([D | Ds], Es, R, X) when D == $-; + D == $+; + D >= $0, D =< $9 -> + scan_exponent(Ds, [D | Es], R, X). + +scan_exponent([], _Es, _R, X) -> {more, X}; +scan_exponent(eof, Es, R, _X) -> + X = R * math:pow(10, list_to_integer(lists:reverse(Es))), + {done, {ok, X}, eof}; +scan_exponent([D | Ds], Es, R, X) when D >= $0, D =< $9 -> + scan_exponent(Ds, [D | Es], R, X); +scan_exponent(Rest, Es, R, _X) -> + X = R * math:pow(10, list_to_integer(lists:reverse(Es))), + {done, {ok, X}, Rest}. + +scan_comment([]) -> {more, "/"}; +scan_comment(eof) -> {done, eof, []}; +scan_comment([$/ | Rest]) -> scan_cpp_comment(Rest); +scan_comment([$* | Rest]) -> scan_c_comment(Rest). + +%% Ignore up to next CR or LF. If the line ends in CRLF, +%% the LF will be treated as separate whitespace, which is +%% okay since it will also be ignored. + +scan_cpp_comment([]) -> {more, "//"}; +scan_cpp_comment(eof) -> {done, eof, []}; +scan_cpp_comment([$\r | Rest]) -> {done, [], Rest}; +scan_cpp_comment([$\n | Rest]) -> {done, [], Rest}; +scan_cpp_comment([_ | Rest]) -> scan_cpp_comment(Rest). + +scan_c_comment([]) -> {more, "/*"}; +scan_c_comment(eof) -> {done, eof, []}; +scan_c_comment([$*]) -> {more, "/**"}; +scan_c_comment([$*, $/ | Rest]) -> {done, [], Rest}; +scan_c_comment([_ | Rest]) -> scan_c_comment(Rest). + +%%% PARSING +%%% +%%% The decode function takes a char list as input, but +%%% interprets the end of the list as only an end to the available +%%% input, and returns a "continuation" requesting more input. +%%% When additional characters are available, they, and the +%%% continuation, are fed into decode/2. You can use the atom 'eof' +%%% as a character to signal a true end to the input stream, and +%%% possibly flush out an unfinished number. The decode_string/1 +%%% function appends 'eof' to its input and calls decode/1. +%%% +%%% Parsing and scanning errors are handled only by match failures. +%%% The external caller must take care to wrap the call in a "catch" +%%% or "try" if better error-handling is desired. Eventually parse +%%% or scan errors will be returned explicitly with a description, +%%% and someday with line numbers too. +%%% +%%% The parsing code uses a continuation-passing style to allow +%%% for the parsing to suspend at any point and be resumed when +%%% more input is available. +%%% See http://en.wikipedia.org/wiki/Continuation_passing_style + +%% Return the first JSON value decoded from the input string. +%% The string must contain at least one complete JSON value. + +decode_string(CharList) -> + {done, V, _} = decode([], CharList ++ eof), + V. + +%% Attempt to decode a JSON value from the input string +%% and continuation, using empty list for the initial continuation. +%% Return {done, Result, LeftoverChars} if a value is recognized, +%% or {more, Continuation} if more input characters are needed. +%% The Result can be {ok, Value}, eof, or {error, Reason}. +%% The Continuation is then fed as an argument to decode/2 when +%% more input is available. +%% Use the atom 'eof' instead of a char list to signal +%% a true end to the input, and may flush a final number. + +decode([], CharList) -> + decode(first_continuation(), CharList); + +decode(Continuation, CharList) -> + {OldChars, Kt} = Continuation, + get_token(OldChars ++ CharList, Kt). + +first_continuation() -> + {[], fun + (eof, Cs) -> + {done, eof, Cs}; + (T, Cs) -> + parse_value(T, Cs, fun(V, C2) -> + {done, {ok, V}, C2} + end) + end}. + +%% Continuation Kt must accept (TokenOrEof, Chars) + +get_token(Chars, Kt) -> + case token(Chars) of + {done, {ok, T}, Rest} -> Kt(T, Rest); + {done, eof, Rest} -> Kt(eof, Rest); + {done, {error, Reason}, Rest} -> {done, {error, Reason}, Rest}; + {more, X} -> {more, {X, Kt}} + end. + +%% Continuation Kv must accept (Value, Chars) + +parse_value(eof, C, _Kv) -> {done, {error, premature_eof}, C}; +parse_value(true, C, Kv) -> Kv(true, C); +parse_value(false, C, Kv) -> Kv(false, C); +parse_value(null, C, Kv) -> Kv(null, C); +parse_value(S, C, Kv) when is_list(S) -> Kv(S, C); +parse_value(N, C, Kv) when is_number(N) -> Kv(N, C); +parse_value(lcbrace, C, Kv) -> parse_object(C, Kv); +parse_value(lsbrace, C, Kv) -> parse_array(C, Kv); +parse_value(_, C, _Kv) -> {done, {error, syntax_error}, C}. + +%% Continuation Kv must accept (Value, Chars) + +parse_object(Chars, Kv) -> + get_token(Chars, fun(T, C2) -> + Obj = obj_new(), + case T of + rcbrace -> Kv(Obj, C2); % empty object + _ -> parse_object(Obj, T, C2, Kv) % token must be string + end + end). + +parse_object(_Obj, eof, C, _Kv) -> + {done, {error, premature_eof}, C}; + +parse_object(Obj, S, C, Kv) when is_list(S) -> % S is member name + get_token(C, fun + (colon, C2) -> + parse_object2(Obj, S, C2, Kv); + (T, C2) -> + {done, {error, {expecting_colon, T}}, C2} + end); + +parse_object(_Obj, M, C, _Kv) -> + {done, {error, {member_name_not_string, M}}, C}. + +parse_object2(Obj, S, C, Kv) -> + get_token(C, fun + (eof, C2) -> + {done, {error, premature_eof}, C2}; + (T, C2) -> + parse_value(T, C2, fun(V, C3) -> % V is member value + Obj2 = obj_store(S, V, Obj), + get_token(C3, fun + (rcbrace, C4) -> + Kv(Obj2, C4); % "}" end of object + (comma, C4) -> % "," another member follows + get_token(C4, fun(T3, C5) -> + parse_object(Obj2, T3, C5, Kv) + end); + (eof, C4) -> + {done, {error, premature_eof}, C4}; + (T2, C4) -> + {done, {error, {expecting_comma_or_curly, T2}}, C4} + end) + end) + end). + +%% Continuation Kv must accept (Value, Chars) + +parse_array(C, Kv) -> + get_token(C, fun + (eof, C2) -> {done, {error, premature_eof}, C2}; + (rsbrace, C2) -> Kv({}, C2); % empty array + (T, C2) -> parse_array([], T, C2, Kv) + end). + +parse_array(E, T, C, Kv) -> + parse_value(T, C, fun(V, C2) -> + E2 = [V | E], + get_token(C2, fun + (rsbrace, C3) -> % "]" end of array + Kv(list_to_tuple(lists:reverse(E2)), C3); + (comma, C3) -> % "," another value follows + get_token(C3, fun(T3, C4) -> + parse_array(E2, T3, C4, Kv) + end); + (eof, C3) -> + {done, {error, premature_eof}, C3}; + (T2, C3) -> + {done, {error, {expecting_comma_or_close_array, T2}}, C3} + end) + end). + +%%% OBJECTS +%%% +%%% We'll use tagged property lists as the internal representation +%%% of JSON objects. Unordered lists perform worse than trees for +%%% lookup and modification of members, but we expect objects to be +%%% have only a few members. Lists also print better. + +%% Is this a proper JSON object representation? + +is_obj({json_object, Props}) when is_list(Props) -> + lists:all(fun + ({Member, _Value}) when is_atom(Member); is_list(Member) -> true; + (_) -> false + end, Props); + +is_obj(_) -> + false. + +%% Create a new, empty object. + +obj_new() -> + {json_object, []}. + +%% Fetch an object member's value, expecting it to be in the object. +%% Return value, runtime error if no member found with that name. + +obj_fetch(Key, {json_object, Props}) when is_list(Props) -> + case proplists:get_value(Key, Props) of + undefined -> + exit({json_object_no_key, Key}); + Value -> + Value + end. + +%% Fetch an object member's value, or indicate that there is no such member. +%% Return {ok, Value} or 'error'. + +obj_find(Key, {json_object, Props}) when is_list(Props) -> + case proplists:get_value(Key, Props) of + undefined -> + error; + Value -> + {ok, Value} + end. + +obj_is_key(Key, {json_object, Props}) -> + proplists:is_defined(Key, Props). + +%% Store a new member in an object. Returns a new object. + +obj_store(Key, Value, {json_object, Props}) when is_list(Props) -> + {json_object, [{Key, Value} | proplists:delete(Key, Props)]}. + +%% Create an object from a list of Key/Value pairs. + +obj_from_list(Props) -> + Obj = {json_object, Props}, + case is_obj(Obj) of + true -> Obj; + false -> exit(json_bad_object) + end. + +%% Fold Fun across object, with initial accumulator Acc. +%% Fun should take (Value, Acc) as arguments and return Acc. + +obj_fold(Fun, Acc, {json_object, Props}) -> + lists:foldl(Fun, Acc, Props). + +%%% TESTING +%%% +%%% We can't expect to round-trip from JSON -> Erlang -> JSON, +%%% due to the degrees of freedom in the JSON syntax: whitespace, +%%% and ordering of object members. We can, however, expect to +%%% round-trip from Erlang -> JSON -> Erlang, so the JSON parsing +%%% tests will in fact test the Erlang equivalence of the +%%% JSON -> Erlang -> JSON -> Erlang coding chain. + +%% Test driver. Return 'ok' or {failed, Failures}. + +test() -> + E2Js = e2j_test_vec(), + Failures = lists:foldl(fun({E, J}, Fs) -> + case (catch test_e2j(E, J)) of + ok -> + case (catch round_trip(E)) of + ok -> + case (catch round_trip_one_char(E)) of + ok -> Fs; + Reason -> [{round_trip_one_char, E, Reason} | Fs] + end; + Reason -> + [{round_trip, E, Reason} | Fs] + end; + Reason -> + [{erlang_to_json, E, J, Reason} | Fs] + end; + (end_of_tests, Fs) -> Fs end, [], E2Js), + case Failures of + [] -> ok; + _ -> {failed, Failures} + end. + +%% Test for conversion from Erlang to JSON. Note that unequal strings +%% may represent equal JSON data, due to discretionary whitespace, +%% object member order, trailing zeroes in floating point, etc. +%% Legitimate changes to the encoding routines may require tweaks to +%% the reference JSON strings in e2j_test_vec(). + +test_e2j(E, J) -> + J2 = lists:flatten(encode(E)), + J = J2, % raises error if unequal + ok. + +%% Test that Erlang -> JSON -> Erlang round-trip yields equivalent term. + +round_trip(E) -> + J2 = lists:flatten(encode(E)), + {ok, E2} = decode_string(J2), + true = equiv(E, E2), % raises error if false + ok. + +%% Round-trip with one character at a time to test all continuations. + +round_trip_one_char(E) -> + J = lists:flatten(encode(E)), + {done, {ok, E2}, _} = lists:foldl(fun(C, Ret) -> + case Ret of + {done, _, _} -> Ret; + {more, Cont} -> decode(Cont, [C]) + end + end, {more, first_continuation()}, J ++ [eof]), + true = equiv(E, E2), % raises error if false + ok. + +%% Test for equivalence of Erlang terms. +%% Due to arbitrary order of construction, equivalent objects might +%% compare unequal as erlang terms, so we need to carefully recurse +%% through aggregates (tuples and objects). + +equiv({json_object, Props1}, {json_object, Props2}) -> + equiv_object(Props1, Props2); +equiv(T1, T2) when is_tuple(T1), is_tuple(T2) -> + equiv_tuple(T1, T2); +equiv(N1, N2) when is_number(N1), is_number(N2) -> N1 == N2; +equiv(S1, S2) when is_list(S1), is_list(S2) -> S1 == S2; +equiv(true, true) -> true; +equiv(false, false) -> true; +equiv(null, null) -> true. + +%% Object representation and traversal order is unknown. +%% Use the sledgehammer and sort property lists. + +equiv_object(Props1, Props2) -> + L1 = lists:keysort(1, Props1), + L2 = lists:keysort(1, Props2), + Pairs = lists:zip(L1, L2), + true = lists:all(fun({{K1, V1}, {K2, V2}}) -> + equiv(K1, K2) and equiv(V1, V2) + end, Pairs). + +%% Recursively compare tuple elements for equivalence. + +equiv_tuple({}, {}) -> + true; +equiv_tuple(T1, T2) when size(T1) == size(T2) -> + S = size(T1), + lists:all(fun(I) -> + equiv(element(I, T1), element(I, T2)) + end, lists:seq(1, S)). + +e2j_test_vec() -> [ + {1, "1"}, + {3.1416, "3.14160"}, % text representation may truncate, trail zeroes + {-1, "-1"}, + {-3.1416, "-3.14160"}, + {12.0e10, "1.20000e+11"}, + {1.234E+10, "1.23400e+10"}, + {-1.234E-10, "-1.23400e-10"}, + {"foo", "\"foo\""}, + {"foo" ++ [500] ++ "bar", [$", $f, $o, $o, 500, $b, $a, $r, $"]}, + {"foo" ++ [5] ++ "bar", "\"foo\\u0005bar\""}, + {"", "\"\""}, + {[], "\"\""}, + {"\n\n\n", "\"\\n\\n\\n\""}, + {obj_new(), "{}"}, + {obj_from_list([{"foo", "bar"}]), "{\"foo\":\"bar\"}"}, + {obj_from_list([{"foo", "bar"}, {"baz", 123}]), + "{\"foo\":\"bar\",\"baz\":123}"}, + {{}, "[]"}, + {{{}}, "[[]]"}, + {{1, "foo"}, "[1,\"foo\"]"}, + + % json array in a json object + {obj_from_list([{"foo", {123}}]), + "{\"foo\":[123]}"}, + + % json object in a json object + {obj_from_list([{"foo", obj_from_list([{"bar", true}])}]), + "{\"foo\":{\"bar\":true}}"}, + + % fold evaluation order + {obj_from_list([{"foo", {}}, + {"bar", obj_from_list([{"baz", true}])}, + {"alice", "bob"}]), + "{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}"}, + + % json object in a json array + {{-123, "foo", obj_from_list([{"bar", {}}]), null}, + "[-123,\"foo\",{\"bar\":[]},null]"}, + + end_of_tests +]. + +%%% TODO: +%%% +%%% Measure the overhead of the CPS-based parser by writing a conventional +%%% scanner-parser that expects all input to be available. +%%% +%%% JSON has dropped comments - disable their parsing. +%%% +%%% Allow a compile-time option to decode object member names as atoms, +%%% to reduce the internal representation overheads when communicating +%%% with trusted peers. diff --git a/sessions/doc.session b/sessions/doc.session index c000402..6b25f56 100755 --- a/sessions/doc.session +++ b/sessions/doc.session @@ -7,5 +7,8 @@ buffer.2.path=/home/gburri/projets/euphorik/doc/technique.txt buffer.2.position=1 buffer.3.path=/home/gburri/projets/euphorik/doc/TODO.txt -buffer.3.position=1 +buffer.3.position=751 buffer.3.current=1 + +buffer.4.path=/home/gburri/projets/euphorik/doc/protocole3.txt +buffer.4.position=1 diff --git a/start_yaws.sh b/start_yaws.sh new file mode 100755 index 0000000..c949292 --- /dev/null +++ b/start_yaws.sh @@ -0,0 +1,2 @@ +#!/bin/bash +yaws --conf /etc/yaws/yaws.conf --sname yaws --mnesiadir "/home/gburri/projets/euphorik/BD/" -I debian_yaws