Patron de conception "Interpréteur" avec Delphi 7

Jérémy LAURENT

Le patron de conception « Interpréteur » est un modèle visant à décrire et utiliser un langage spécifique pour réaliser des opérations en lien avec un domaine particulier. Ce langage est généralement propre au domaine auquel il est lié. Il est communément transmis dans une simple chaîne de caractères, car le texte brut est plus facile à lire et à manipuler. L’interpréteur est couramment utilisé pour analyser des chaînes algébriques et en renvoyer le résultat.

Commentez

- Titre : Patron de conception "Interpréteur" avec Delphi 7

- Auteur : Jérémy LAURENT

- Parution : 8 avril 2021

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2019 Jérémy LAURENT. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Préambule

- II. Structure

- III. Grammaire

- III-A. EBNF

- III-B. EBNF appliqué à JSON

- IV. Implémentation des expressions

- IV-A. Expressions terminales de données

- IV-B. Expressions non terminales vues comme terminales

- IV-C. Expressions non terminales basiques

- V. Construction de l’arbre

- V-A. Construction brute

- V-B. Construction calculée

- V-B-1. Les jetons

- V-B-2. Le parseur

- V-B-3. L’analyseur syntaxique

- V-B-4. Le code d’appel

- VI. Interprétation

- VI-A. Contrat du contexte

- VI-B. Appel au contexte

- VI-C. Comportement du contexte

- VII. Avantages / Inconvénients du patron

- VII-A. Avantages

- VII-B. Inconvénients

- VIII. Liens

- IX. Remerciements

I - Préambule

Dans ce tutoriel nous allons aborder l’un des patrons comportementaux présentés par le Gang of Four (cf. Liens) : l’Interpréteur. Nous créerons petit à petit un projet tournant autour du JSON.

Le choix du JSON s’est porté sur sa popularité et sa syntaxe minimaliste.

Attention, je ne prétends pas que l’interpréteur est la meilleure solution pour traiter du JSON. Il s’agit simplement d’un sujet qui parlera à la majorité des lecteurs et qui est suffisamment complexe pour raccrocher tous les points que je souhaite aborder.

II - Structure

Dans ce patron de conception, tout tourne autour du langage.
Les éléments qui composent un langage sont la grammaire de celui-ci.

Avant toute autre chose, il faut décrire cette grammaire au moyen de règles où chaque règle sera soit une expression terminale, soit une expression non terminale.
La différence réside dans le fait qu’une expression terminale peut être évaluée directement alors qu’une expression non terminale devra être décomposée en expressions terminales et/ou non terminales. Finalement, ces expressions vont former un arbre où chaque expression non terminale représente une branche et où chaque expression terminale représente une feuille.

L’interprétation du langage en lui-même nécessitera souvent l’utilisation d’un objet annexe afin d’orchestrer les différentes opérations à effectuer ou tout simplement pour stocker des informations. Dans le patron de conception Interpréteur, il est désigné sous le terme « Contexte ». Ce contexte peut être omis s’il n’est pas nécessaire, mais je vous conseille de toujours l’incorporer, car il est rare qu’une grammaire reste figée.

Ce qui nous amène à la structure suivante :

III - Grammaire

Pour une implémentation efficace du patron Interpréteur, il est absolument nécessaire d’avoir une connaissance exhaustive des règles composant la grammaire du langage.

Il existe d’ailleurs des langages dont les règles servent à décrire les règles d’un langage (j’espère n’avoir perdu personne). Pour la suite, EBNF sera utilisé pour décrire la grammaire du JSON.

III-A - EBNF

EBNF (pour « Extended Backus-Naur Form ») est un code qui exprime la grammaire d'un langage formel.
Un EBNF se compose d’expressions terminales et de règles de production d’expressions non terminales, qui sont les restrictions régissant la manière dont les expressions terminales peuvent être combinées dans une séquence légale.

Le format EBNF définit les règles de production dans lesquelles des séquences d’expressions terminales et non terminales sont respectivement attribuées à une expression non terminale.

Exemple EBNF

Non terminal 1 = “Terminal 1”, [“Terminal 2”, Non terminal 2|“Terminal 3”, Non terminal 3], {“Terminal 4”} ;
Non terminal 2 = ?0041?.. ?7A? - ?005B?..?0060?;
Non terminal 3 = “Terminal 5”, {“Terminal 6”, “Terminal 5”} ;

Le tableau suivant résume les différents caractères utilisés et leurs significations :

Caractère

Signification

=

Définition

,

Concaténation

;

Fin de la règle

|

Alternative

[ABC]

Option qui est présente 0 ou 1 fois

{ABC}

Option qui est présente 0 ou n fois

(ABC)

Groupe solidaire (nécessaire avec l’alternative si l’une d’entre elles est composée d’une concaténation)

“ABC”

Expression terminale

(*ABC*)

Commentaire

?ABC?

Séquence spéciale

..

Plage d’alternatives (utilisé pour éviter l’énumération complète d’une liste d’expressions terminales où chacune des expressions doit être considérée comme étant de même type).

Ex. : “1”..“9” remplace l’expression “1”|“2”|“3”|“4”|“5”|“6”|“7”|“8”|“9” et chacune de ces expressions terminales doit être vue comme un chiffre.

-

Exclusion (va de pair avec la plage d’alternatives pour en exclure des entrées spécifiques)

III-B - EBNF appliqué à JSON

Pour le JSON, la syntaxe est minimaliste. On est donc susceptible de penser que sa grammaire l’est également. Faisons donc un tour sur le site officiel pour voir ce qu’il en est réellement (cf. Liens).

Dans la partie gauche, nous voyons des textes explicatifs et des schémas décrivant la grammaire.
Dans la partie droite, nous voyons la grammaire en elle-même. Notez qu’il ne s’agit pas d’EBNF classique, mais du format McKeeman (cf. Liens). Cette syntaxe minimaliste supprime la plupart des caractères de contrôle de la syntaxe EBNF dans le but de la simplifier (comme la virgule, les crochets et les accolades), mais oblige à les remplacer par d’autres mécanismes basés sur des espaces signifiants. Cela a un impact direct sur les noms donnés aux expressions non terminales, car ils ne peuvent plus contenir d’espaces, et sur la longueur des expressions qui ne peuvent plus dépasser un certain nombre de caractères, obligeant à revenir à la ligne. Dans le cadre du JSON, ces contraintes ne dérangent pas trop. Mais sur certains langages plus évolués comme SQL, cela devient très vite contraignant. C’est pour cela que cette forme simplifiée n’est pas utilisée officiellement (je vous invite à visiter la documentation Transact-SQL pour voir par vous-même) pour décrire une grammaire et que ce n’est pas celle qui sera utilisée dans cet article.

À ce stade, nous pouvons soit prendre pour argent comptant la grammaire décrite, soit essayer de comprendre comment on est arrivé à cette grammaire.

Le site officiel a évolué depuis la première ébauche de ce tutoriel. Les schémas présentés seront donc sensiblement différents. La principale différence est la prise en compte des espaces blancs (espaces, tabulations, sauts de ligne et retours chariot) dans la grammaire. Bien que cette représentation soit plus représentative de la réalité, ces espaces blancs sont simplement des caractères parasites qui n’ont aucun rôle actif dans la syntaxe (sauf dans les chaînes de caractères). On notera au passage que les schémas des expressions « Object » et « Array » du site présentent tous deux une expression « value » dans leur composition, alors que la grammaire décrite à côté parle d’expression « Element » pour « Object » et « Elements » pour « Array ». Ce qui fait qu’il manque quelques espaces blancs dans ces schémas. Pour ces deux raisons, mais également pour simplifier, les anciens schémas sont volontairement utilisés dans ce document, mais cela signifie que la syntaxe à laquelle nous aboutirons sera légèrement différente.

Voici la description d’un Objet JSON :

An object is an unordered set of name/value pairs. An object begins with { (left brace) and ends with } (right brace). Each name is followed by : (colon) and the name/value pairs are separated by , (comma).

Et voici son schéma :

Il s’agit d’un schéma qui indique les routes à suivre pour former un Objet JSON.
On commence par une accolade ouvrante allant soit directement vers une accolade fermante (première route), soit vers une chaîne de caractères suivie du caractère « : », suivi d’une valeur (deuxième route). La deuxième route bifurque soit vers la fin de la première soit vers une troisième qui passe par une virgule avant de revenir au début de la deuxième. Bref, elle décrit graphiquement ce qui est marqué dans la description.

En y regardant de plus près, on remarque que les accolades ouvrantes et fermantes, les deux-points et la virgule ont des bords arrondis, alors que la chaîne de caractères et la valeur ont des bords droits (c’est encore le cas sur le schéma actuellement en ligne). Cela n’est pas fait ainsi par hasard, cette simple différence permet au lecteur de repérer du premier coup d’œil quels éléments sont des expressions terminales (bords arrondis) et quels éléments sont des expressions non terminales (bords droits).

On peut commencer la grammaire, le premier chemin étant très simple :

Objet JSON : Premier chemin uniquement :

Object = “{”, “}”;

Pour prendre en compte le deuxième chemin, on le place entre crochets, car il est optionnel. String et Value sont des expressions non terminales, elles auront forcément une description plus loin. Pour le moment ceci devrait suffire.

Objet JSON : Premier et deuxième chemin

Object = “{”, [String, “:”, Value], “}”;

Pour prendre en compte le troisième chemin optionnel, il faut également prendre en compte son caractère itératif et l’obligation de passer par une virgule dans ce cas-là.

Objet JSON : Tous les chemins, syntaxe lourde

Object = “{”, [String, “:”, Value], {“,”, String, “:”, Value}, “}”;

Dans cette grammaire, on peut constater une répétition de la suite formée par les expressions String, COLON et Value. Cela laisse à penser que cette suite doit avoir sa propre expression non terminale. Cela est d’ailleurs suggéré dans la description par « set of name/value pairs » (une collection de paires clé/valeur). Il faut donc ajouter une entrée dans la grammaire pour la collection en elle-même et une autre pour représenter la paire, puis simplifier la première ligne.

Objet JSON : Tous les chemins, syntaxe régulière

Object = “{”, [Members], “}”;
Members = Pair, {“,”, Pair};
Pair = String, “:”, Value;

Avec cette grammaire, on couvre le schéma et la description de l’Objet JSON.
Cependant, même en laissant de côté les espaces blancs, cette grammaire reste différente de celle proposée sur le site.

Notre approche est basée sur une répétition conditionnelle du duo virgule-paire (une simple itération conditionnée par la présence de la virgule) où la structure est composée d’une unique collection de paires clé/valeur et colle parfaitement à la définition.

L’approche proposée par le site est récursive. La structure en découlant sera composée d’une paire clé/valeur et d’une seconde structure elle-même composée d’une paire clé/valeur et d’une structure elle-même composée… Bon, je crois que vous avez compris le principe.

Cette approche récursive n’est pas le fruit du hasard.
L’explication simple est que puisqu’il n’y a pas de symbole pour exprimer la répétition dans la forme McKeeman, la récursivité est un moyen simple de le faire.
Une autre explication, basée sur la manière dont sont construits la plupart des algorithmes d’analyse (parseurs), est plus complexe et s’éloigne du sujet de ce tutoriel. Si vous êtes curieux (ou courageux) vous pouvez lire l’excellent blog de Josh Haberman (cf. Liens).

Transformons une dernière fois notre grammaire pour avoir également une approche récursive. Notez que j’ai renommé « Member » en « Pair » pour éviter des confusions entre « Members » et « Member ».

Objet JSON : Tous les chemins, syntaxe optimisée

Object = “{”, [Members], “}”;
Members = Pair, [“,”, Members];
Pair = String, “:”, Value;

Maintenant que nous avons les bases, nous passerons plus vite sur les autres descriptions et schémas. À commencer par le tableau.

La description d’un tableau JSON est la suivante :

An array is an ordered collection of values. An array begins with [ (left bracket) and ends with ] (right bracket). Values are separated by , (comma).

Et voici son schéma :

Sur les mêmes principes que précédemment, la grammaire qui en découle est la suivante. Notez une différence avec la grammaire proposée par le site, induite par le fait que nous ignorons les espaces blancs :

Tableau JSON

Array = “[”, [Elements], “]”;
Elements = Value, [“,”, Elements];

La description d’une valeur JSON est la suivante :

A value can be a string in double quotes, or a number, or true or false or null, or an object or an array. These structures can be nested.

Et voici son schéma :

String est toujours une expression non terminale et Number l’est également. Donc, pour le moment on se contente de ce qu’on a. Object et Array sont déjà décrites. Ne restent que les valeurs « true », « false » et « null » :

Valeur JSON

Value = String|Number|Object|Array|“true”|“false”|“null”;

La description de String est la suivante :

A string is a sequence of zero or more Unicode characters, wrapped in double quotes, using backslash escapes. A character is represented as a single character string. A string is very much like a C or Java string.

Son schéma montre que ses règles d’écritures sont sensiblement plus compliquées que ce que nous avons vu jusque-là :

Nous allons donc prendre le temps de le décomposer, en commençant par le cas de la chaîne vide :

Chaîne JSON : Cas chaîne vide

String = “””, “””;

Compliquons les choses en amenant la suite de caractères standards (en gros tous les caractères dont les codes Unicode vont de 0020 à 10FFFF sauf le guillemet et le caractère d’échappement).

Chaîne JSON : Cas caractères standards

String = “””, [Characters], “””;
Characters = SingleCharacter, [Characters];
SingleCharacter = ?0020?..?10FFFF? – “”” – “\” (*Any character except double quote and backslash*)

Les derniers chemins passent tous par le caractère d’échappement suivi d’un caractère échappé. Le dernier caractère échappé (u) doit être suivi par une expression non terminale formée de quatre caractères hexadécimaux.

Vous noterez deux petites différences.
J’inclus le caractère d’échappement dans la grammaire comme faisant partie du caractère spécial comme c’est le cas en C et en Java, afin de mieux coller à la description.
Je nomme « Unicode » l’expression non terminale commençant par « \u ».

Chaîne JSON : grammaire complète

String = “””, [Characters], “””;
Characters = SingleCharacter, [Characters];
SingleCharacter = ?0020?..?10FFFF? – “”” – “\” (*Any character except double quote and backslash*) | Escape;
Escape = “\”” | “\\” | “\/”| “\b” | “\f” | “\n” | “\r” | “\t” | Unicode;
Unicode = “\u”, Hexadecimal, Hexadecimal, Hexadecimal, Hexadecimal;

Pour le moment je m’arrête sans préciser ce qui se cache derrière l’hexadécimal comme cela est le cas dans le schéma. Nous y reviendrons plus tard.

La description de Number est la suivante :

A number is very much like a C or Java number, except that the octal and hexadecimal formats are not used.

Le schéma de Number est le plus complexe :

Décomposons ce schéma en commençant par le chemin le plus simple (celui qui passe par 0 et contourne tout le reste) :

Nombre JSON : Cas pour les nuls

Number = “0” ;

Allons un peu plus loin en regardant de plus près le schéma. Nous pouvons constater qu’il n’est plus possible de revenir en arrière lorsqu’on arrive à l’intersection se trouvant juste avant le point. Nous observons la même chose à l’intersection prenant soit le chemin vers « e » et « E », soit le chemin vers la fin. Cela nous indique que nous avons trois expressions non terminales pour former le nombre JSON : la partie entière, la partie décimale optionnelle et l’exposant optionnel.

Nombre JSON : Séparation entier, décimal, exposant

Number = Integer, [Decimal], [Exponent] ;

La partie entière peut commencer ou non par le signe négatif, qui sera donc optionnel. Il sera suivi soit par un zéro, soit par un chiffre allant de 1 à 9, le bord droit dans le schéma indiquant clairement qu’il faut avoir une expression non terminale pour les chiffres de 1 à 9.

Nombre JSON : Partie entière (début)

Integer = [“-”], “0”| OneNineDigit ;
OneNineDigit = “1”..“9” ;

Dans le cas du nombre commençant par un chiffre allant de 1 à 9, nous avons ensuite une répétition de chiffres (incluant le 0 puisque pas de précision) et suggérant une approche récursive de la même manière que pour les caractères :

Nombre JSON : Partie entière (complète)

Integer = [“-”], “0” | (OneNineDigit, [Digits]);
Digits = SingleDigit, [Digits];
SingleDigit = “0”|OneNineDigit;
OneNineDigit = “1”..“9”;

Vous noterez ici, une importante différence entre la grammaire proposée par le site et celle que j’ai déduite du schéma. J’ai remplacé l’expression SingleDigit par une expression « 0 » dans la composition de l’expression Integer, car SingleDigit implique également les chiffres de 1 à 9. Mettre SingleDigit ici n’est pas faux, mais ça implique tout de même que deux chemins commencent par la même condition pour ensuite nécessiter un autre test afin de déterminer si la condition est le chiffre 0, indiquant qu’il faut alors chercher le point décimal, ou bien si c’est un chiffre strictement positif, indiquant que nous pouvons avoir d’autres chiffres derrière. Autant donner l’indication directement.

C’est comme si j’étais à vélo sur une route et que deux panneaux allant dans des directions opposées m’indiquaient tous les deux la ville où je veux aller, l’une des routes passant par une départementale et l’autre passant par une autoroute avec des aires de repos. L’autoroute a un panneau bleu et me permettra de m’arrêter au restaurant de l’aire de repos, mais elle est interdite aux vélos. La route passant par la départementale a un panneau blanc et je peux l’emprunter avec mon vélo, mais je ne pourrai pas aller au restaurant de l’aire de repos. Ici, c’est le même principe. Si l’entier commence par un zéro, je ne peux pas passer par un chiffre strictement positif pour rejoindre le point décimal.

Gérons à présent les décimales. Là encore le chemin revient sur lui-même indiquant une répétition 1 à n fois et suggérant également une approche récursive qui est déjà gérée par Digits :

Nombre JSON : Partie décimale

Decimal = “.”, Digits;

L’exposant va obligatoirement passer par « e » ou « E ».

Nombre JSON : Partie exposant (Début)

Exponent = “e”| “E” ;

Il sera suivi par un signe optionnel (plus ou moins) :

Nombre JSON : Partie exposant avec signe (Incomplet)

Exponent = “e”| “E”, [“+”| “-”] ;

Et se terminera obligatoirement par une répétition de chiffres :

Nombre JSON : Partie exposant complète

Exponent = “e”| “E”, [“+”| “-”], Digits ;

Il ne reste plus qu’à revenir sur le cas d’un caractère hexadécimal. Il n’est pas inclus dans le schéma, mais il faut obligatoirement qu’il soit présent dans la grammaire. Il s’agit simplement d’un chiffre de 0 à 1 ou d’une lettre de A à F indépendamment de la casse (qu’il faudra toutefois prévoir).

Hexadécimal

Hexadecimal = “0”..“9” | “a”..“f” | “A”..“F”;

Établir la grammaire n’est pas toujours un procédé facile et on peut potentiellement y passer beaucoup de temps suivant la complexité du langage que l’on décrit. Pour le JSON, on ne soupçonne pas la complexité des éléments qui semblent les plus simples au premier abord et on arrive à avoir autant de lignes pour décrire un nombre ou une chaîne qu’il en faut pour décrire tout le reste.

Grammaire du JSON (complète)

Object = “{”, [Members], “}”;
Members = Pair, [“,”, Members];
Pair = String, “:”, Value;
Array = “[”, [Elements], “]”;
Elements = Value, [“,”, Elements];
Value = String|Number|Object|Array|“true”|“false”|“null”;

String = “””, [Characters], “””;
Characters = SingleCharacter, [Characters];
SingleCharacter = ?0020?..?10FFFF? – “”” – “\” (*Any character except double quote and backslash*) | Escape;
Escape = “\”” | “\\” | “\/”| “\b” | “\f” | “\n” | “\r” | “\t” | Unicode;
Unicode = “\u”, Hexadecimal, Hexadecimal, Hexadecimal, Hexadecimal;
Hexadecimal = “0”..“9” | “a”..“f” | “A”..“F”;

Number = Integer, [Decimal], [Exponent];
Integer = [“-”], “0” | (OneNineDigit, [Digits]);
Decimal = “.”, Digits;
Exponent = “e”|“E”, [“+”|“-”], Digits;
Digits = SingleDigit, [Digits];
SingleDigit = “0”|OneNineDigit;
OneNineDigit = “1”..“9”;

IV - Implémentation des expressions

Une fois que la grammaire est établie, chaque ligne la composant représente une expression non terminale. Cela signifie que chaque ligne d’expression non terminale devra généralement avoir sa propre classe.

Parmi les expressions terminales (celles qui sont entre des guillemets), il faut repérer celles représentant de la donnée constante, car elles vont aussi avoir leurs propres classes. Normalement, on connaît déjà cette information lorsqu’on écrit la grammaire.

Il ne faut pas perdre de vue l’objectif final qui est l’interprétation. Par conséquent, il faut se poser la question suivante : « Est-ce que le contexte a besoin de tout le détail de la grammaire ou est-ce qu’il existe des cas où il a uniquement besoin d’une simple donnée qui est masquée par la complexité de certaines expressions terminales ? » Si pour l’une des expressions non terminales, la réponse à cette question est positive, cela signifie que cette expression non terminale devra être vue par le contexte comme une expression terminale.

Ceci est la théorie, voyons la pratique.

Avant toute autre chose, il convient de définir la classe représentant l’expression abstraite. Elle contiendra une unique méthode, abstraite elle aussi, prenant potentiellement en paramètre un contexte et renvoyant ou non une valeur selon le besoin. Comme précédemment, je conseille de placer systématiquement un contexte.

En règle générale, le contexte prend la forme d’une classe abstraite afin de rester ouvert à l’extension. Pour ma part, je préfère utiliser une interface, car le compilateur vérifiera le contrat, ce qu’il ne fera pas avec une classe abstraite. Et dans un premier temps, on se contentera de sa déclaration pour pouvoir compiler.

Expression abstraite utilisant le contrat d’un contexte

IJsonContext = interface
['{210BA2C9-0EEE-423F-85C1-61E101EB4CC6}']
end;

TJsonExpression = class
private
public
procedure Interpret(Context : IJsonContext); virtual; abstract;
end;

Toutes les autres expressions ou presque vont hériter de cette classe abstraite et redéfinir la méthode d’interprétation.

IV-A - Expressions terminales de données

Une expression terminale de donnée est très facile à implémenter, car son contenu est en dur dans la grammaire. Pour le JSON, il s’agit des expressions true, false et null.

Déclarations des expressions terminales

TJsonTrueExpression = class(TJsonExpression)
private
public
procedure Interpret(Context : IJsonContext); override;
end;

TJsonFalseExpression = class(TJsonExpression)
private
public
procedure Interpret(Context : IJsonContext); override;
end;

TJsonNullExpression = class(TJsonExpression)
private
public
procedure Interpret(Context : IJsonContext); override;
end;

Implémentations des expressions terminales

{ TJsonTrueExpression }

procedure TJsonTrueExpression.Interpret(Context : IJsonContext);
begin
WriteLn('True');
end;

{ TJsonFalseExpression }

procedure TJsonFalseExpression.Interpret(Context : IJsonContext);
begin
WriteLn('False');
end;

{ TJsonNullExpression }

procedure TJsonNullExpression.Interpret(Context : IJsonContext);
begin
WriteLn('Null');
end;

IV-B - Expressions non terminales vues comme terminales

Une expression non terminale peut être vue comme une expression terminale par le contexte et nécessiter une interprétation qui lui est propre avant d’être transmise. Pour JSON, les expressions String et Number en font partie puisque le contexte a seulement besoin de la donnée, indépendamment de la complexité nécessaire pour l’obtenir.

L’interprétation de String va logiquement renvoyer du texte et l’interprétation de Number également puisque le contexte peut avoir besoin de la valeur avec l’exposant. Nous avons donc une autre classe abstraite à déclarer :

Abstraction de l’interprétation de String et Number

TJsonVariableDataExpression = class
private
public
function Interpret : String; virtual; abstract;
end;

Pour définir String, il faut au préalable définir ses composants en commençant par le plus bas au niveau de sa grammaire : l’expression hexadécimale.

Puisqu’elle entre dans la composition de String, on la fait hériter de la nouvelle classe abstraite. Elle contiendra obligatoirement un unique caractère hexadécimal à passer dans le constructeur.

Expression hexadécimale (déclaration)

TJsonHexadecimalExpression = class(TJsonVariableDataExpression)
private
FHexadecimal : String;
public
constructor Create(Const Hexadecimal : String);

function Interpret : String; override;
end;

Le constructeur vérifie qu’on lui passe bien un unique caractère hexadécimal et la méthode d’interprétation se contentera de renvoyer ce caractère.

Expression hexadécimale (implémentation)

{ TJsonHexadecimalExpression }

constructor TJsonHexadecimalExpression.Create(const Hexadecimal : String);
Const
Allowed = ['0','1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'a', 'b', 'c', 'd', 'e', 'f'];
begin
if (Length(Hexadecimal) <> 1) then
raise TJsonUnexpectedHexadecimalLengthException.Create;

if not (Hexadecimal[1] in Allowed) then
raise TJsonUnexpectedHexadecimalCharacterException.Create(Hexadecimal);

FHexadecimal := Hexadecimal;
end;

function TJsonHexadecimalExpression.Interpret: String;
begin
Result := FHexadecimal;
end;

L’expression hexadécimale est obligatoire quatre fois dans l’expression Unicode, ce qui implique de les demander dans le constructeur.

Expression Unicode (déclaration)

TJsonUnicodeExpression = class(TJsonVariableDataExpression)
private
FFirst : TJsonHexadecimalExpression;
FSecond : TJsonHexadecimalExpression;
FThird : TJsonHexadecimalExpression;
FFourth : TJsonHexadecimalExpression;
public
constructor Create(Const First, Second, Third, Fourth : TJsonHexadecimalExpression);

function Interpret : String; override;
end;

Je pars du principe que si un paramètre est obligatoire, je ne dois pas prendre le risque qu’il soit absent lorsque j’en aurai besoin. Mon constructeur s’assure donc que les expressions hexadécimales soient bien assignées. La méthode d’interprétation demandera simplement à chaque expression hexadécimale de lui fournir sa propre interprétation avant de les concaténer derrière un « \u ».

Expression Unicode (implémentation)

{ TJsonUnicodeExpression }

constructor TJsonUnicodeExpression.Create(const First, Second, Third, Fourth : TJsonHexadecimalExpression);
begin
if not(Assigned(First)) or not(Assigned(Second)) or not(Assigned(Third)) or not(Assigned(Fourth)) then
raise TJsonMissingHexadecimalsException.Create;

FFirst := First; FSecond := Second;
FThird := Third; FFourth := Fourth;
end;

function TJsonUnicodeExpression.Interpret: String;
begin
Result := '\u' + FFirst.Interpret + FSecond.Interpret + FThird.Interpret + FFourth.Interpret;
end;

Notez que je ne transforme pas cette information.
Je ne peux pas présumer de ce qu’en fera le contexte alors je lui laisse la responsabilité de le faire s’il en a besoin.

L’expression Unicode est utilisée dans l’expression Escape et fait partie d’une liste de choix où toutes les autres possibilités sont des expressions terminales en dur dans la grammaire. Cela suggère que cette classe aura deux constructeurs.

Expression Escape (déclaration)

TJsonEscapeExpression = class(TJsonVariableDataExpression)
private
FCharacter : String;
FUnicode : TJsonUnicodeExpression;
public
constructor Create(Const Character : String); overload;
constructor Create(Unicode : TJsonUnicodeExpression);  overload;

function Interpret : String; override;
end;

En appliquant les mêmes principes que précédemment cela donne l’implémentation suivante :

Expression Escape (implémentation)

{ TJsonEscapeExpression }

constructor TJsonEscapeExpression.Create(const Character: String);
begin
FCharacter := Character;
end;

constructor TJsonEscapeExpression.Create(Unicode: TJsonUnicodeExpression);
begin
if not(Assigned(Unicode)) then
raise TJsonMissingUnicodeException.Create;

FUnicode := Unicode;
end;

function TJsonEscapeExpression.Interpret: String;
begin
if (Assigned(FUnicode)) then Result := FUnicode.Interpret
else Result := FCharacter;
end;

L’expression Escape est utilisée dans l’expression SingleCharacter et fait partie d’une liste de choix où toutes les autres possibilités sont des expressions terminales en dur dans la grammaire (puisqu’on peut remplacer la plage par une liste exhaustive des valeurs). Bref, on a déjà vu comment faire dans cette situation.

Expression SingleCharacter (déclaration)

TJsonSingleCharacterExpression = class(TJsonVariableDataExpression)
private
FCharacter : String;
FEscape : TJsonEscapeExpression;
public
constructor Create(Const Character : String); overload;
constructor Create(Escape : TJsonEscapeExpression);  overload;

function Interpret : String; override;
end;

L’unique différence réside dans l’objet passé en paramètre, mais l’implémentation reste la même.

Expression SingleCharacter (implémentation)

{ TJsonSingleCharacterExpression }

constructor TJsonSingleCharacterExpression.Create(const Character: String);
begin
FCharacter := Character;
end;

constructor TJsonSingleCharacterExpression.Create(Escape: TJsonEscapeExpression);
begin
if not(Assigned(FEscape)) then
raise TJsonMissingEscapeException.Create;

FEscape := Escape;
end;

function TJsonSingleCharacterExpression.Interpret : String;
begin
if (Assigned(FEscape)) then Result := FEscape.Interpret
else Result := FCharacter;
end;

L’expression SingleCharacter est obligatoirement utilisée dans l’expression Characters, elle sera donc demandée dans son constructeur. Mais l’expression Characters peut potentiellement inclure une autre expression Characters. Ce caractère optionnel induit que je ne dois pas nécessairement l’inclure dans le constructeur, mais prévoir une méthode pour l’ajouter si besoin.

Expression Characters (déclaration)

TJsonCharactersExpression = class(TJsonVariableDataExpression)
private
FSingleCharacter : TJsonSingleCharacterExpression;
FCharacters : TJsonCharactersExpression;
public
constructor Create(SingleCharacter : TJsonSingleCharacterExpression);
procedure SetCharacters(Characters : TJsonCharactersExpression);

function Interpret : String; override;
end;

L’interprétation sera donc la concaténation de l’interprétation de l’expression SingleCharacter et de l’interprétation de l’autre expression Characters dans le cas où elle est présente.

Expression Characters (implémentation)

{ TJsonCharactersExpression }

constructor TJsonCharactersExpression.Create(SingleCharacter: TJsonSingleCharacterExpression);
begin
if not(Assigned(SingleCharacter)) then
raise TJsonMissingCharacterException.Create;

FSingleCharacter := SingleCharacter;
end;

procedure TJsonCharactersExpression.SetCharacters(Characters: TJsonCharactersExpression);
begin
FCharacters := Characters;
end;

function TJsonCharactersExpression.Interpret: String;
begin
Result := FSingleCharacter.Interpret;
if (Assigned(FCharacters)) then
Result := Result + FCharacters.Interpret;
end;

Notez que je ne contrôle l’assignation de l’expression Characters que lorsque je souhaite l’utiliser. Je pars du principe de ne pas bloquer le client si ce n’est pas nécessaire. D’autres préfèreront renvoyer une exception si le paramètre Characters n’est pas assigné lors de l’utilisation de la méthode SetCharacters, prétextant que si le consommateur veut ajouter cette expression à l’arbre, il doit veiller à ce qu’elle soit assignée, et ils auront également raison.

Enfin l’expression Characters est potentiellement utilisée dans l’expression String.
Cette expression sera utilisée par le contexte, il ne faut donc pas la faire hériter de l’abstraction d’interprétation de texte, mais bien de l’abstraction d’interprétation globale. Et puisque l’interprétation interne du texte n’est pas connue par le contexte et ne doit pas l’être, il faut prévoir un accesseur pour qu’il puisse tout de même obtenir le résultat final de cette opération.

Expression String (déclaration)

TJsonStringExpression = class(TJsonExpression)
private
FStringValue : String;
FCharacters : TJsonCharactersExpression;

function GetValue : String;
public
property Value : String read GetValue;

constructor Create;
procedure SetCharacters(Characters : TJsonCharactersExpression);
procedure Interpret(Context : IJsonContext); override;
end;

L’interprétation nécessitera simplement de vérifier la présence de l’expression Characters et d’appeler la méthode d’interprétation de celle-ci.

Expression String (implémentation)

{ TJsonStringExpression }

constructor TJsonStringExpression.Create;
begin
FStringValue := '';
end;

function TJsonStringExpression.GetValue: String;
begin
Result := FStringValue;
end;

procedure TJsonStringExpression.SetCharacters(Characters: TJsonCharactersExpression);
begin
FCharacters := Characters;
end;

procedure TJsonStringExpression.Interpret(Context : IJsonContext);
begin

if (Assigned(FCharacters)) then
FStringValue := FCharacters.Interpret;

WriteLn('String, Value = ' + FStringValue);
end;

Pour définir Number, il faut au préalable définir ses composants en commençant par le plus bas au niveau de sa grammaire : l’expression OneNineDigit.

Puisqu’elle entre dans la composition de Number, on la fait hériter de la classe abstraite d’interprétation d’une chaîne. Elle contiendra obligatoirement un unique chiffre sous forme de texte à passer dans le constructeur, suivi d’une autre expression Digits optionnelle qui nécessitera donc une méthode d’ajout.

Expression OneNineDigit (déclaration)

TJsonOneNineDigitExpression = class(TJsonVariableDataExpression)
private
FDigit : String;
public
constructor Create(Const Digit : String);
function Interpret : String; override;
end;

Le constructeur vérifiera qu’on a bien passé un chiffre de 1 à 9 et l’interprétation se contentera de renvoyer cette valeur.

Expression OneNineDigit (implémentation)

{ TJsonOneNineDigitExpression }

constructor TJsonOneNineDigitExpression.Create(const Digit: String);
begin
if not(InRange(StrToIntDef(Digit, 0), 1, 9)) then
raise TJsonUnexpectedOneNineDigitException.Create(Digit);

FDigit := Digit;
end;

function TJsonOneNineDigitExpression.Interpret: String;
begin
Result := FDigit;
end;

L’expression OneNineDigit entre dans la composition de l’expression SingleDigit et de Integer. Integer dépendant d’autres expressions, on crée l’expression SingleDigit qui est soit l’expression terminale du zéro, soit notre expression OneNineDigit (deux options signifiant encore une fois deux constructeurs).

Expression SingleDigit (déclaration)

TJsonSingleDigitExpression = class(TJsonVariableDataExpression)
private
FOneNineDigit : TJsonOneNineDigitExpression;
public
constructor Create; overload;
constructor Create(OneNineDigit : TJsonOneNineDigitExpression); overload;

function Interpret : String; override;
end;

Pour l’interprétation, la présence d’une expression OneNineDigit déterminera si on rend la valeur « 0 » ou l’interprétation de OneNineDigit.

Expression SingleDigit (implémentation)

{ TJsonSingleDigitExpression }

constructor TJsonSingleDigitExpression.Create;
begin
Create(nil);
end;

constructor TJsonSingleDigitExpression.Create(OneNineDigit: TJsonOneNineDigitExpression);
begin
FOneNineDigit := OneNineDigit;
end;

function TJsonSingleDigitExpression.Interpret: String;
begin
if (Assigned(FOneNineDigit)) then Result := FOneNineDigit.Interpret
else Result := '0';
end;

L’expression SingleDigit entre dans la constitution de l’expression Digits. Elle est construite sur le même principe que l’expression Characters, on aura une déclaration similaire.

Expression Digits (déclaration)

TJsonDigitsExpression =  class(TJsonVariableDataExpression)
private
FFirstDigit : TJsonSingleDigitExpression;
FFollowingDigits : TJsonDigitsExpression;
public
constructor Create(Const FirstDigit : TJsonSingleDigitExpression);
procedure SetFollowingDigits(Digits : TJsonDigitsExpression);
function Interpret : String; override;
end;

Idem pour l’implémentation.

Expression Digits (implémentation)

{ TJsonDigitsExpression }

constructor TJsonDigitsExpression.Create(const FirstDigit: TJsonSingleDigitExpression);
begin
FFirstDigit := FirstDigit;
end;

procedure TJsonDigitsExpression.SetFollowingDigits(Digits: TJsonDigitsExpression);
begin
FFollowingDigits := Digits;
end;

function TJsonDigitsExpression.Interpret: String;
begin
Result := FFirstDigit.Interpret;
if (Assigned(FFollowingDigits)) then
Result := Result + FFollowingDigits.Interpret;
end;

L’expression Exponent est constituée d’une expression terminale en dur que nous n’avons pas besoin d’expliciter, puis d’un signe optionnel (suggérant un deuxième constructeur pour savoir si l’exposant est négatif ou bien une méthode pour définir le signe), puis d’une expression Digits obligatoire (donc présente dans les deux constructeurs si on choisit cette implémentation) :

Expression Exponent (déclaration)

TJsonExponentExpression = class(TJsonVariableDataExpression)
private
FValue : String;
FDigits : TJsonDigitsExpression;
public
constructor Create(Digits : TJsonDigitsExpression); overload;
constructor Create(Digits : TJsonDigitsExpression; Const IsNegative : Boolean); overload;

function Interpret : String; override;
end;

Pour l’interprétation l’expression terminale est systématiquement placée dans le résultat, suivi du signe négatif si nécessaire, suivi de l’interprétation de l’expression Digits.

Expression Exponent (implémentation)

{ TJsonExponentExpression }

constructor TJsonExponentExpression.Create(Digits: TJsonDigitsExpression);
begin
Create(Digits, False);
end;

constructor TJsonExponentExpression.Create(Digits: TJsonDigitsExpression; const IsNegative: Boolean);
begin
if not(Assigned(Digits)) then
raise TJsonMissingDigitsException.Create;

FValue := 'e';
if (IsNegative) then
FValue := FValue + '-';

FDigits := Digits;
end;

function TJsonExponentExpression.Interpret: String;
begin
Result := FValue + FDigits.Interpret;
end;

L’expression Decimal est formée d’une expression terminale en dur que nous n’avons pas besoin d’expliciter et d’une expression Digits obligatoire.

Expression Decimal (déclaration)

TJsonDecimalExpression = class(TJsonVariableDataExpression)
private
FDigits : TJsonDigitsExpression;
public
constructor Create(Digits : TJsonDigitsExpression);
function Interpret : String; override;
end;

Expression Decimal (implémentation)

{ TJsonDecimalExpression }

constructor TJsonDecimalExpression.Create(Digits : TJsonDigitsExpression);
begin
if not(Assigned(Digits)) then
raise TJsonMissingDigitsException.Create;

FDigits := Digits;
end;

function TJsonDecimalExpression.Interpret: String;
begin
Result := '.' + FDigits.Interpret;
end;

L’expression Integer est la plus complexe. Elle commence par un signe négatif optionnel suivi soit par une expression terminale « 0 » qui doit être seule et terminer l’expression, soit par une expression OneNineDigit potentiellement suivie d’une expression Digits. Nous aurons donc un constructeur prenant en paramètre une expression SingleDigit pour le zéro et un autre prenant en paramètre une expression OneNineDigit. L’interprétation sera faite en fonction de la présence de l’une ou l’autre. Pour ses deux constructeurs, on ajoute un paramètre supplémentaire indiquant si l’entier est négatif. Nous aurons une méthode pour ajouter les digits supplémentaires si nécessaire.

Expression Integer (déclaration)

TJsonIntegerExpression = class(TJsonVariableDataExpression)
private
FNegative : Boolean;
FZeroDigit : TJsonSingleDigitExpression;
FOneNineDigit : TJsonOneNineDigitExpression;
FFollowingDigits : TJsonDigitsExpression;
public
constructor Create(Const Negative : Boolean; ZeroDigit : TJsonSingleDigitExpression); overload;
constructor Create(Const Negative : Boolean; OneNineDigit : TJsonOneNineDigitExpression); overload;

procedure SetFollowingDigits(Digits : TJsonDigitsExpression);

function Interpret : String; override;
end;

Pour l’interprétation, la présence du signe négatif est fonction du booléen correspondant. Et comme dit précédemment, on a soit une expression SingleDigit, soit une expression OneNineDigit, mais jamais les deux. Dans le deuxième cas, on vérifie l’assignation de l’expression Digits pour savoir si on doit ajouter des chiffres.

Expression Integer (implémentation)

{ TJsonIntegerExpression }

constructor TJsonIntegerExpression.Create(const Negative: Boolean; ZeroDigit: TJsonSingleDigitExpression);
begin
if not(Assigned(ZeroDigit)) then
raise TJsonMissingSingleDigitException.Create;

FNegative := Negative;
FZeroDigit := ZeroDigit;
end;

constructor TJsonIntegerExpression.Create(const Negative: Boolean; OneNineDigit: TJsonOneNineDigitExpression);
begin
if not(Assigned(OneNineDigit)) then
raise TJsonMissingOneNineDigitException.Create;

FNegative := Negative;
FOneNineDigit := OneNineDigit;
end;

procedure TJsonIntegerExpression.SetFollowingDigits(Digits: TJsonDigitsExpression);
begin
FFollowingDigits := Digits;
end;

function TJsonIntegerExpression.Interpret: String;
begin
Result := '';
if (FNegative) then
Result := '-';

if (Assigned(FZeroDigit)) then
Result := Result + FZeroDigit.Interpret
else
begin
Result := Result + FOneNineDigit.Interpret;
if (Assigned(FFollowingDigits)) then
Result := Result + FFollowingDigits.Interpret;
end;
end;

Enfin, nous avons l’expression Number formée d’un entier obligatoire, puis d’un décimal optionnel, puis d’un exposant optionnel. Nous aurons donc un constructeur prenant l’expression Integer en paramètre et deux méthodes pour affecter les expressions Decimal et Exponent. Cette expression sera utilisée par le contexte, il ne faut donc pas la faire hériter de l’abstraction d’interprétation de texte, mais bien de l’abstraction d’interprétation globale. Et puisque l’interprétation interne du texte n’est pas connue par le contexte et ne doit pas l’être, il faut prévoir un accesseur pour qu’il puisse tout de même obtenir le résultat final de cette opération. On ne renvoie pas de nombre, c’est le contexte qui le transformera si besoin.

Expression Number (déclaration)

TJsonNumberExpression = class(TJsonExpression)
private
FStringValue : String;

FInteger : TJsonIntegerExpression;
FDecimal : TJsonDecimalExpression;
FExponent : TJsonExponentExpression;

function GetValue : String;
public
property Value : String read GetValue;

constructor Create(Integer_ : TJsonIntegerExpression);

procedure SetDecimal(Decimal : TJsonDecimalExpression);
procedure SetExponent(Exponent : TJsonExponentExpression);

procedure Interpret(Context : IJsonContext); override;
end;

L’implémentation n’a rien de compliqué, on se contente d’appeler les méthodes d’interprétation de chacun des composants du nombre.

Expression Number (implémentation)

{ TJsonNumberExpression }

constructor TJsonNumberExpression.Create(Integer_: TJsonIntegerExpression);
begin
if not(Assigned(Integer_)) then
raise TJsonMissingIntegerException.Create;

FInteger := Integer_;

FStringValue := '';
end;

function TJsonNumberExpression.GetValue: String;
begin
Result := FStringValue;
end;

procedure TJsonNumberExpression.SetDecimal(Decimal: TJsonDecimalExpression);
begin
FDecimal := Decimal;
end;

procedure TJsonNumberExpression.SetExponent(Exponent: TJsonExponentExpression);
begin
FExponent := Exponent;
end;

procedure TJsonNumberExpression.Interpret(Context : IJsonContext);
begin
FStringValue := FInteger.Interpret;

if (Assigned(FDecimal)) then
FStringValue := FStringValue + FDecimal.Interpret;

if (Assigned(FExponent)) then
FStringValue := FStringValue + FExponent.Interpret;

WriteLn('Number, Value = ' + FStringValue);
end;

IV-C - Expressions non terminales basiques

Une expression non terminale basique est une expression non terminale qui ne doit pas être vue comme une expression terminale par le contexte et ne doit pas nécessiter une interprétation séparée. Bref, ce sont les plus courantes et si elles apparaissent en dernier dans ce tutoriel, c’est uniquement, car Delphi a besoin que les autres expressions soient déclarées afin de les incorporer dans les expressions terminales basiques. Pour JSON, resteront donc les expressions non terminales Value, Elements, Array, Pair, Members et Object.

Si on regarde de plus près l’expression Value et sa définition, on remarque qu’il s’agit uniquement d’un terme générique pour désigner soit un Object, soit un String, soit un Number, soit un Array, soit une expression terminale de données. Cela signifie que lorsque l’arbre d’expression sera construit, l’expression Value ne sera jamais instanciée puisqu’on instanciera les autres expressions.

Dans la structure des classes, cela signifie que nous disposons de deux options :

- déclarer une classe héritant de TJsonExpression et en faire dériver les expressions Object, Array, String, Number, True, False, et Null ;

- ignorer cette expression et dériver les expressions Object, Array, String, Number, True, False, et Null directement de TJsonExpression.

Pour ne pas complexifier inutilement l’exemple, parce que le JSON est normé et parce que dans ce cas particulier, ça ne change pas grand-chose, nous garderons la deuxième option. Cependant, ce genre de décision doit être réfléchi. L’option de facilité est rarement la meilleure.

Commençons par l’expression Elements qui contient une expression Value obligatoire (donc à passer dans le constructeur), suivie potentiellement par une suite formée par l’expression terminale de la virgule qui n’a pas de raison d’être explicitée et par une autre expression Elements. Notez que nous ne connaissons pas le type que prendra Value, nous demanderons donc un TJsonExpression et laisserons le polymorphisme faire le reste.

Expression Elements (déclaration)

TJsonElementsExpression = class(TJsonExpression)
private
FValue : TJsonExpression;
FElements : TJsonElementsExpression;
public
constructor Create(Value : TJsonExpression);

procedure SetElements(Elements : TJsonElementsExpression);
procedure Interpret(Context : IJsonContext); override;
end;

L’interprétation ne diffère guère de ce que nous avons vu jusque-là.

Expression Elements (implémentation

{ TJsonElementsExpression }

constructor TJsonElementsExpression.Create(Value: TJsonExpression);
begin
if not(Assigned(Value)) then
raise TJsonMissingValueException.Create;

FValue := Value;
end;

procedure TJsonElementsExpression.SetElements(Elements: TJsonElementsExpression);
begin
FElements := Elements;
end;

procedure TJsonElementsExpression.Interpret(Context : IJsonContext);
begin
Writeln('Elements');

FValue.Interpret(Context);
if (Assigned(FElements)) then
FElements.Interpret(Context);
end;

Les autres expressions sont construites selon les principes que nous avons vus et détaillés pour toutes les expressions précédentes. Nous n’entrerons donc pas dans les détails qui ont permis d’obtenir le code suivant :

Expressions Array, Pair, Members et Object (déclarations)

TJsonArrayExpression = class(TJsonExpression)
private
FElements : TJsonElementsExpression;
public
procedure SetElements(Elements : TJsonElementsExpression);
procedure Interpret(Context : IJsonContext); override;
end;

TJsonPairExpression = class(TJsonExpression)
private
FName : TJsonStringExpression;
FValue : TJsonExpression;

function GetNameValue : String;
public
property NameValue : String read GetNameValue;

constructor Create(Name : TJsonStringExpression; Value : TJsonExpression);

procedure Interpret(Context : IJsonContext); override;
end;

TJsonMembersExpression = class(TJsonExpression)
private
FPair : TJsonPairExpression;
FMembers : TJsonMembersExpression;
public
constructor Create(Pair : TJsonPairExpression);

procedure SetMembers(Members : TJsonMembersExpression);
procedure Interpret(Context : IJsonContext); override;
end;

TJsonObjectExpression = class(TJsonExpression)
private
FMembers : TJsonMembersExpression;
public
procedure SetMembers(Members : TJsonMembersExpression);

procedure Interpret(Context : IJsonContext); override;
end;

Expressions Array, Pair, Members et Object (implémentations)

{ TJsonArrayExpression }

procedure TJsonArrayExpression.SetElements(Elements: TJsonElementsExpression);
begin
FElements := Elements;
end;

procedure TJsonArrayExpression.Interpret(Context : IJsonContext);
begin
Writeln('Array');

if (Assigned(FElements)) then
FElements.Interpret(Context);

end;

{ TJsonPairExpression }

constructor TJsonPairExpression.Create(Name : TJsonStringExpression; Value: TJsonExpression);
begin
FName := Name;
FValue := Value;
end;

function TJsonPairExpression.GetNameValue: String;
begin
Result := FName.Value;
end;

procedure TJsonPairExpression.Interpret(Context : IJsonContext);
begin
FName.Interpret(Context);
Writeln('Pair, Identifier = ' + FName.Value);
FValue.Interpret(Context);
end;

{ TJsonMembersExpression }

constructor TJsonMembersExpression.Create(Pair: TJsonPairExpression);
begin
if not(Assigned(Pair)) then
raise TJsonMissingPairException.Create;

FPair := Pair;
end;

procedure TJsonMembersExpression.SetMembers(Members: TJsonMembersExpression);
begin
FMembers := Members;
end;

procedure TJsonMembersExpression.Interpret(Context : IJsonContext);
begin
Writeln('Members');

FPair.Interpret(Context);
if (Assigned(FMembers)) then
FMembers.Interpret(Context);
end;

{ TJsonObjectExpression }

procedure TJsonObjectExpression.SetMembers(Members : TJsonMembersExpression);
begin
FMembers := Members;
end;

procedure TJsonObjectExpression.Interpret(Context : IJsonContext);
begin
Writeln('Object');

if (Assigned(FMembers)) then
FMembers.Interpret(Context);
end;

V - Construction de l’arbre

Comme indiqué au chapitre II, la construction de l’arbre ne fait pas partie du patron de conception à proprement parler. Cette étape est cependant essentielle, car sans arbre, nous n’avons rien à interpréter.

La méthode importe peu à partir du moment où la grammaire est respectée.
Nous pouvons construire un arbre manuellement à partir de rien, ou bien imaginer un algorithme qui le construit.

V-A - Construction brute

Le code qui suit part d’une page blanche et utilise directement les objets correspondant aux différentes expressions pour former un arbre dont l’interprétation est sensée produire un JSON avec la structure suivante :

Construction d’un arbre à la main

ObjectExpression := TJsonObjectExpression.Create;

PairNameExpression := TJsonStringExpression.Create;
PairNameExpression.SetCharacters(TJsonCharactersExpression.Create(TJsonSingleCharacterExpression.Create('String')));
StringExpression := TJsonStringExpression.Create;
StringExpression.SetCharacters(TJsonCharactersExpression.Create(TJsonSingleCharacterExpression.Create('Value')));
PairExpression := TJsonPairExpression.Create(PairNameExpression, StringExpression);
FirstMembersExpression := TJsonMembersExpression.Create(PairExpression);

PairNameExpression := TJsonStringExpression.Create;
PairNameExpression.SetCharacters(TJsonCharactersExpression.Create(TJsonSingleCharacterExpression.Create('Integer')));
NumberExpression := TJsonNumberExpression.Create(TJsonIntegerExpression.Create(False, TJsonSingleDigitExpression.Create(TJsonOneNineDigitExpression.Create('1'))));
DigitsExpression := TJsonDigitsExpression.Create(TJsonSingleDigitExpression.Create(TJsonOneNineDigitExpression.Create('5')));
DigitsExpression.SetFollowingDigits(TJsonDigitsExpression.Create(TJsonSingleDigitExpression.Create(TJsonOneNineDigitExpression.Create('2'))));
DecimalExpression := TJsonDecimalExpression.Create(DigitsExpression);
NumberExpression.SetDecimal(DecimalExpression);
ExponentExpression := TJsonExponentExpression.Create(TJsonDigitsExpression.Create(TJsonSingleDigitExpression.Create(TJsonOneNineDigitExpression.Create('4'))));
NumberExpression.SetExponent(ExponentExpression);
PairExpression := TJsonPairExpression.Create(PairNameExpression, NumberExpression);
ParentMembersExpression := TJsonMembersExpression.Create(PairExpression);
FirstMembersExpression.SetMembers(ParentMembersExpression);

PairNameExpression := TJsonStringExpression.Create;
PairNameExpression.SetCharacters(TJsonCharactersExpression.Create(TJsonSingleCharacterExpression.Create('TrueBoolean')));
TrueExpression := TJsonTrueExpression.Create;
PairExpression := TJsonPairExpression.Create(PairNameExpression, TrueExpression);
ChildMembersExpression := TJsonMembersExpression.Create(PairExpression);
ParentMembersExpression.SetMembers(ChildMembersExpression);
ParentMembersExpression := ChildMembersExpression;

PairNameExpression := TJsonStringExpression.Create;
PairNameExpression.SetCharacters(TJsonCharactersExpression.Create(TJsonSingleCharacterExpression.Create('FalseBoolean')));
FalseExpression := TJsonFalseExpression.Create;
PairExpression := TJsonPairExpression.Create(PairNameExpression, FalseExpression);
ChildMembersExpression := TJsonMembersExpression.Create(PairExpression);
ParentMembersExpression.SetMembers(ChildMembersExpression);
ParentMembersExpression := ChildMembersExpression;

PairNameExpression := TJsonStringExpression.Create;
PairNameExpression.SetCharacters(TJsonCharactersExpression.Create(TJsonSingleCharacterExpression.Create('NullValue')));
NullExpression := TJsonNullExpression.Create;
PairExpression := TJsonPairExpression.Create(PairNameExpression, NullExpression);
ChildMembersExpression := TJsonMembersExpression.Create(PairExpression);
ParentMembersExpression.SetMembers(ChildMembersExpression);
ParentMembersExpression := ChildMembersExpression;

PairNameExpression := TJsonStringExpression.Create;
PairNameExpression.SetCharacters(TJsonCharactersExpression.Create(TJsonSingleCharacterExpression.Create('Array')));
ArrayExpression := TJsonArrayExpression.Create;
StringExpression := TJsonStringExpression.Create;
StringExpression.SetCharacters(TJsonCharactersExpression.Create(TJsonSingleCharacterExpression.Create('Other Value')));
FirstElementsExpression := TJsonElementsExpression.Create(StringExpression);
IntegerExpression := TJsonIntegerExpression.Create(False, TJsonOneNineDigitExpression.Create('1'));
DigitsExpression :=  TJsonDigitsExpression.Create(TJsonSingleDigitExpression.Create(TJsonOneNineDigitExpression.Create('2')));
DigitsExpression.SetFollowingDigits(TJsonDigitsExpression.Create(TJsonSingleDigitExpression.Create(TJsonOneNineDigitExpression.Create('3'))));
IntegerExpression.SetFollowingDigits(DigitsExpression);
NumberExpression := TJsonNumberExpression.Create(IntegerExpression);
DigitsExpression :=  TJsonDigitsExpression.Create(TJsonSingleDigitExpression.Create(TJsonOneNineDigitExpression.Create('4')));
DigitsExpression.SetFollowingDigits(TJsonDigitsExpression.Create(TJsonSingleDigitExpression.Create(TJsonOneNineDigitExpression.Create('5'))));
NumberExpression.SetDecimal(TJsonDecimalExpression.Create(DigitsExpression));
ParentElementsExpression := TJsonElementsExpression.Create(NumberExpression);
FirstElementsExpression.SetElements(ParentElementsExpression);
ChildElementsExpression := TJsonElementsExpression.Create(TrueExpression);
ParentElementsExpression.SetElements(ChildElementsExpression);
ParentElementsExpression := ChildElementsExpression;
ChildElementsExpression := TJsonElementsExpression.Create(FalseExpression);
ParentElementsExpression.SetElements(ChildElementsExpression);
ParentElementsExpression := ChildElementsExpression;
ChildElementsExpression := TJsonElementsExpression.Create(NullExpression);
ParentElementsExpression.SetElements(ChildElementsExpression);
ArrayExpression.SetElements(FirstElementsExpression);
PairExpression := TJsonPairExpression.Create(PairNameExpression, ArrayExpression);
ChildMembersExpression := TJsonMembersExpression.Create(PairExpression);
ParentMembersExpression.SetMembers(ChildMembersExpression);

ObjectExpression.SetMembers(FirstMembersExpression);

C’est compliqué n’est-ce pas…
C’est la raison pour laquelle cet arbre est généralement construit à partir de l’analyse de quelque chose d’autre.

V-B - Construction calculée

La production d’un algorithme de construction dépendra du point de départ. On peut vouloir créer cet arbre à partir d’une base de données et d’un contexte qui transformera cet arbre en chaîne JSON. Ou encore faire l’inverse : partir d’une chaîne JSON et utiliser un contexte chargé de mettre à jour la base de données.

La procédure décrite ci-après explique en détail la manière dont j’opère personnellement pour construire un arbre à partir d’un texte brut et d’une grammaire. Encore une fois, je n’irai pas jusqu’à avancer qu’il s’agit de la méthode la plus efficace, mais elle a le mérite de reposer sur des règles simples à comprendre et à mettre en œuvre.

Comme nous l’avons vu précédemment, les expressions terminales sont constantes. Pour JSON, un délimiteur de texte sera toujours un guillemet double, le début d’un tableau sera toujours un crochet ouvrant (qui ne se trouve pas entre des guillemets doubles), la valeur « true » sera toujours écrite de la même façon, etc. Ces expressions terminales vont donc servir de points de repère pour déterminer quelle expression non terminale nous allons rencontrer et vont également servir de critères de contrôle pour déterminer si la grammaire est bien respectée. Par la suite, je nommerai « jeton », une expression terminale déterminante dans la grammaire.

Puisque nous partons d’un texte brut, il faut une mécanique pour traduire chaque caractère rencontré en jeton. Ce sera le rôle d’une classe nommée « Analyseur syntaxique ». Elle connaîtra les jetons et le texte brut complet, mais, sauf s’il n’y a pas moyen de faire autrement, elle ne connaîtra pas la grammaire à l’exception des espaces blancs et des caractères constituant les jetons.

Il faut ensuite, une mécanique pour orchestrer la suite de jetons et construire l’arbre d’expressions en fonction des jetons et de la grammaire. Ce sera le rôle d’une classe nommée « Parseur » qui connaîtra donc les jetons, l’analyseur et surtout la grammaire, mais n’aura aucune connaissance du texte brut à l’exception de la portion à l’origine du jeton en cours.

Enfin nous aurons une classe pour représenter les jetons qui ne connaîtra rien d’autre que les données propres à un jeton.

V-B-1 - Les jetons

Pour JSON, les jetons sont les suivants :

- accolades ouvrantes et fermantes ;

- crochets ouvrants et fermants ;

- virgule, deux-points, guillemets doubles ;

- valeurs « true », « false » et « null » ;

- caractère d’échappement, caractères de contrôle d’échappement ;

- zéro, chiffres de 1 à 9, point décimal, signes positif et négatif, exposant.

À ceux-ci, je vais ajouter :

- un jeton de fin de fichier pour signifier que je n’ai plus rien à analyser ;

- un jeton « non géré » pour traiter le cas où je rencontre quelque chose que je ne sais pas analyser ;

- un jeton « valeur » pour avoir quelque chose de générique en cas de problème rencontré dans l’analyse d’une chaîne, d’un nombre ou d’une valeur « true », « false », et « null ».

Chacun de ces jetons va correspondre dans le code à une valeur d’énumération sur laquelle se basera le parseur pour construire l’arbre.

Énumération des types de jetons

TJsonTokenKind =  (
LeftCurlyBracket, RightCurlyBracket, // Object
LeftSquareBracket, RightSquareBracket, // Array
Comma, Colon, DoubleQuote, // Pair
TrueValue, FalseValue, NullValue, // Data
Character, EscapedDoubleQuote, BackSlash, Slash, // Strings
BackSpace, FormFeed, NewLine, Return, Tab, Unicode, //Strings
Zero, OneNine, Dot, Plus, Less, Exponent, // Number
Value, NotManaged, EndOfFile // System
);

Chacun des jetons va également correspondre à une constante de type chaîne sur laquelle se basera l’analyseur pour associer un caractère à l’une des valeurs de l’énumération. Je n’inclus volontairement pas les constantes des chiffres de 1 à 9 car Delphi possède des routines qui le vérifient sans devoir faire neuf tests.

Constantes des caractères des jetons

const
LEFT_CURLY_BRACKET : String = '{';
RIGHT_CURLY_BRACKET : String = '}';
LEFT_SQUARE_BRACKET : String = '[';
RIGHT_SQUARE_BRACKET : String = ']';

DOUBLE_QUOTE : String = '"';
COLON_CHARACTER : String = ':';
COMMA_CHARACTER : String = ',';
BACKSLASH_CHARACTER : String = '\';

SLASH_CHARACTER : String = '\/';
BACKSPACE_CHARACTER : String = '\b';
FORM_FEED_CHARACTER : String = '\f';
NEW_LINE_CHARACTER : String = '\n';
RETURN_CHARACTER : String = '\r';
TAB_CHARACTER : String = '\t';
UNICODE_CHARACTER : String = '\u';

DOT_CHARACTER : String = '.';
ZERO_CHARACTER : String = '0';
PLUS_SIGN_CHARACTER : String = '+';
LESS_SIGN_CHARACTER : String = '-';
EXPONENT_CHARACTER : String = 'e';

TRUE_VALUE : String = 'true';
FALSE_VALUE : String = 'false';
NULL_VALUE : String = 'null';

WHITE_SPACE : String = ' ';

Notez que l’espace blanc fait partie de ces constantes. Comme dit précédemment, ils ne sont pas déterminants dans la syntaxe et sont donc absents de notre grammaire. Mais ils seront présents dans le texte JSON que va traiter l’analyseur et il faut qu’il les connaisse pour être capable de savoir qu’il ne doit pas les transmettre. Je n’inclus volontairement pas les autres espaces blancs, car Delphi possède des routines qui les renvoient.

La classe représentant un jeton sera donc instanciée par l’analyseur. L’objet en résultant sera transmis au parseur avec la valeur d’énumération et la valeur constante correspondant au jeton trouvé. En cas d’erreur de syntaxe, dans le but de faciliter la correction, on stockera également la position du jeton.

Jeton

TJsonToken = class
private
FKind : TJsonTokenKind;
FValue : String;
FPosition : Integer;
public
constructor Create(Const AKind : TJsonTokenKind; Const AValue : String; Const APosition : Integer);

property Kind : TJsonTokenKind read FKind;
property Value : String read FValue;
property Position : Integer read FPosition;
end;

V-B-2 - Le parseur

Le parseur est chargé de recevoir les jetons pour construire l’arbre en respectant scrupuleusement grammaire. Pour cela nous allons appliquer les règles suivantes et revoir le code si nécessaire :

- le parseur n’est pas responsable de l’analyseur, il se contente de l’utiliser ;

- le parseur ne connaît que le dernier jeton ;

- le parseur n’a qu’une seule méthode publique : celle qui renvoie l’arbre d’expressions ;

- chaque expression non terminale de la grammaire doit avoir sa méthode dédiée renvoyant l’objet d’expression correspondant ;

- chaque virgule ou point-virgule dans la grammaire implique qu’on doit demander le jeton suivant. Ce sera le rôle d’une méthode dédiée. Si on trouve le point-virgule, il faut s’assurer que l’objet est bien retourné par la méthode dans laquelle on se trouve ;

- chaque expression non terminale trouvée dans la composition d’une autre expression doit déclencher un appel à la méthode correspondante puis l’utilisation de son retour dans l’expression courante ;

- chaque pipe dans la grammaire implique une structure Case Of dans le code ;

- chaque parenthèse ouvrante implique que tout le bloc entre cette parenthèse et la parenthèse fermante doit faire partie d’un même bloc d’instructions. Les parenthèses ne sont normalement pas utiles si la ligne de grammaire ne contient pas de pipe ;

- chaque crochet ouvrant dans la grammaire implique une condition dans le code pour vérifier la présence de l’expression optionnelle placée entre les crochets. Si un caractère pipe se trouve entre ces crochets, il faut vérifier les expressions terminales se trouvant de chaque côté. Si une expression non terminale s’y trouve, il faut descendre dans la grammaire jusqu’à trouver les expressions terminales à vérifier ;

- chaque accolade ouvrante dans la grammaire implique le même comportement qu’un crochet ouvrant répété tant qu’au moins une des conditions est vérifiée.

La première règle implique simplement un constructeur demandant l’analyseur et un membre privé pour le stocker. Pour le moment une classe d’analyseur déclarant une unique méthode pour fournir le jeton suffira.

Parseur (règle 1, déclaration)

TJsonParser = class
private
FTokenizer : TJsonTokenizer;
protected
public
constructor Create(Tokenizer : TJsonTokenizer);
end;

Parseur (règle 1 ,implémentation)

constructor TJsonParser.Create(Tokenizer: TJsonTokenizer);
begin
FTokenizer := Tokenizer;
FCurrentToken := FTokenizer.NextToken;
end;

La seconde règle implique simplement un stockage du jeton. Et la troisième implique une méthode publique renvoyant l’expression abstraite d’interprétation globale.

Parseur (règles 2 et 3)

TJsonParser = class
private
FTokenizer : TJsonTokenizer;
FCurrentToken : TJsonToken;
protected
public
constructor Create(Tokenizer : TJsonTokenizer);
function Parse : TJsonExpression;
end;

La quatrième règle nécessite simplement de déclarer les méthodes renvoyant les différentes expressions non terminales, sans préciser ce qu’elles font exactement.

Parseur (règle 4)

TJsonParser = class
private
FTokenizer : TJsonTokenizer;
FCurrentToken : TJsonToken;
protected
function ParseObject : TJsonObjectExpression;
function ParseMembers : TJsonMembersExpression;
function ParsePair : TJsonPairExpression;
function ParseArray : TJsonArrayExpression;
function ParseElements : TJsonElementsExpression;
function ParseValue : TJsonExpression;

function ParseString : TJsonStringExpression;
function ParseChararacters : TJsonCharactersExpression;
function ParseSingleCharacter : TJsonSingleCharacterExpression;
function ParseEscape : TJsonEscapeExpression;
function ParseUnicode : TJsonUnicodeExpression;
function ParseHexadecimal : TJsonHexadecimalExpression;

function ParseNumber : TJsonNumberExpression;
function ParseInteger : TJsonIntegerExpression;
function ParseDecimal : TJsonDecimalExpression;
function ParseExponent : TJsonExponentExpression;
function ParseDigits : TJsonDigitsExpression;
function ParseSingleDigit : TJsonSingleDigitExpression;
function ParseOneNineDigit : TJsonOneNineDigitExpression;
public
constructor Create(Tokenizer : TJsonTokenizer);
function Parse : TJsonExpression;
end;

La cinquième règle implique une méthode pour demander le jeton suivant. Cette méthode prendra en paramètre la valeur d’énumération correspondant au type de jeton qu’on est censé avoir en suivant la grammaire afin de vérifier que c’est bien le cas. Ce code ne montre que l’implémentation, mais il faudra bien entendu la déclarer.

Méthode EatToken

procedure TJsonParser.EatToken(Kind: TJsonTokenKind);
begin
if (FCurrentToken.Kind <> Kind) then
raise TBadTokenException.Create(Kind, FCurrentToken.Kind, FCurrentToken.Position);

FCurrentToken := FTokenizer.NextToken;
end;

Pour les autres règles, cela se fera au fur et à mesure de l’écriture du code. On commence par la méthode publique pour descendre dans la grammaire. Et la grammaire stipule qu’un JSON commence et se termine obligatoirement par un seul unique objet même s’il ne contient rien. Notre procédure publique se contentera donc d’appeler la méthode renvoyant un objet pour ensuite vérifier qu’on est à la fin du fichier (car s’il y a quelque chose derrière la dernière accolade fermante, ce n’est plus du JSON valide).

Méthode Parse

function TJsonParser.Parse: TJsonExpression;
begin
Result := ParseObject;

if (FCurrentToken.Kind <> EndOfFile) then
raise TBadTokenException.Create(EndOfFile, FCurrentToken.Kind, FCurrentToken.Position);
end;

Le tableau ci-dessous reprend la grammaire de l’expression Object et y fait correspondre les règles à appliquer pour écrire le code.

“{”

Expression terminale indiquant la présence d’un objet. Nous sommes dans la procédure gérant les objets donc tout va bien et il n’y a rien à faire ici.

,

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien l’accolade ouvrante. Appel à EatToken(LeftCurlyBracket).

[

Insertion d’une condition sur la présence d’une ou plusieurs expressions terminales indiquant qu’il faut traiter l’expression Members qui suit et ouverture d’un bloc conditionnel dans ce cas. Members commence par Pair qui commence par String qui commence par l’expression terminale du guillemet double. J’ai ma condition d’entrée.

Members

Appel à la méthode ParseMembers et affectation de son résultat à l’expression courante. Cela implique que le résultat soit déjà instancié à ce moment-là où que ce retour soit stocké jusqu’à l’instanciation du résultat.

]

Fin du bloc conditionnel

,

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Puisqu’on ne peut pas savoir quel sera ce jeton (règle 2), l’appel à EatToken devra être effectué par la méthode qui connaîtra cette information.

“}”

Expression terminale indiquant la fin d’un objet. Nous sommes dans la procédure gérant les objets donc tout va bien et il n’y a rien à faire ici.

;

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien l’accolade fermante. Appel à EatToken(RightCurlyBracket). Je m’assure que l’expression a bien été retournée.

Le code ci-dessous reprend simplement les étapes listées dans le tableau précédent.

Méthode ParseObject

function TJsonParser.ParseObject: TJsonObjecTExpression;
begin
Result := TJsonObjecTExpression.Create;

EatToken(LeftCurlyBracket);

if (FCurrentToken.Kind = DoubleQuote) then
Result.SetMembers(ParseMembers);

EatToken(RightCurlyBracket);
end;

Appliquons le même principe pour l’expression Members :

Pair

Appel à la méthode ParsePair et affectation de son résultat à l’expression courante. Cela implique que le résultat soit déjà instancié à ce moment-là ou que ce retour soit stocké jusqu’à l’instanciation du résultat.

,

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Puisqu’on ne peut pas savoir quel sera ce jeton (règle 2), l’appel à EatToken devra être effectué par la méthode qui connaîtra cette information.

[

Insertion d’une condition sur la présence de l’expression terminale de la virgule et ouverture d’un bloc conditionnel dans ce cas.

“,”

Expression terminale indiquant la présence d’une virgule. Rien à faire à ce niveau-là

,

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien la virgule. Appel à EatToken(Comma).

Members

Appel à la méthode ParseMembers et affectation de son résultat à l’expression courante. Cela implique que le résultat soit déjà instancié à ce moment-là ou que ce retour soit stocké jusqu’à l’instanciation du résultat.

]

Fin du bloc conditionnel.

;

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Puisqu’on ne peut pas savoir quel sera ce jeton (règle 2), l’appel à EatToken devra être effectué par la méthode qui connaîtra cette information. Je m’assure que l’expression a bien été retournée.

Vous vous rappelez que nous avons dit que l’approche de cette expression était récursive ? Eh bien c’est confirmé par le tableau ci-dessus et par le code ci-dessous.

Méthode ParseMembers

function TJsonParser.ParseMembers: TJsonMembersExpression;
begin
Result := TJsonMembersExpression.Create(ParsePair);

if (FCurrentToken.Kind = Comma) then
begin
EatToken(Comma);
Result.SetMembers(ParseMembers);
end;
end;

Voici le même tableau pour l’expression Pair :

String

Appel à la méthode ParseString et affectation de son résultat à l’expression courante. Cela implique que le résultat soit déjà instancié à ce moment-là ou que ce retour soit stocké jusqu’à l’instanciation du résultat.

,

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Ici nous savons quel sera le jeton de fin, mais ne fait pas partie de l’expression Pair, mais de l’expression String. On laissera la méthode ParseString s’occuper du EatToken(DoubleQuote).

“:”

Expression terminale indiquant la présence des deux-points. Rien à faire à ce niveau-là.

,

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui des deux-points. Appel à EatToken(Colon).

Value

Appel à la méthode ParseValue et affectation de son résultat à l’expression courante. Cela implique que le résultat soit déjà instancié à ce moment-là ou que ce retour soit stocké jusqu’à l’instanciation du résultat.

;

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Puisqu’on ne peut pas savoir quel sera ce jeton (règle 2), l’appel à EatToken devra être effectué par la méthode qui connaîtra cette information. Je m’assure que l’expression a bien été retournée.

L’unique différence avec ce que nous avons déjà fait est que la valeur retournée par la méthode ParseString est stockée au lieu d’être affectée directement. Cela est dû au fait que le constructeur de l’expression Pair exige deux paramètres et que nous ne pouvons pas évaluer le deuxième sans avoir au préalable pris en compte les deux-points.

Méthode ParsePair

function TJsonParser.ParsePair: TJsonPairExpression;
var NameOperation : TJsonStringExpression;
begin
NameOperation := ParseString;
EatToken(Colon);
Result := TJsonPairExpression.Create(NameOperation, ParseValue);
end;

Passons à l’expression Array :

“[”

Expression terminale indiquant la présence d’un tableau. Il n’y a rien à faire ici.

,

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Appel à EatToken(LeftSquareBracket).

[

Insertion d’une condition sur la présence des expressions terminales pouvant se trouver au début de l’expression non terminale Elements et ouverture d’un bloc conditionnel dans ce cas. Elements commence par Value qui commence par potentiellement beaucoup de choses. Fort heureusement, l’expression Elements est la seule expression entre les crochets. Ce qui fait qu’au lieu de faire plein de conditions pour trouver l’une des nombreuses expressions terminales pouvant se trouver ici, nous pouvons tester l’absence du crochet fermant. Nous déportons donc cette problématique sur la méthode ParseValue.

Elements

Appel à la méthode ParseElements et affectation de son résultat à l’expression courante. Cela implique que le résultat soit déjà instancié à ce moment-là ou que ce retour soit stocké jusqu’à l’instanciation du résultat.

]

Fin du bloc conditionnel.

,

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Puisqu’on ne peut pas savoir quel sera ce jeton (règle 2), l’appel à EatToken devra être effectué par la méthode qui connaîtra cette information.

“]”

Expression terminale indiquant la fin du tableau. Il n’y a rien à faire ici.

;

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Appel à EatToken(RightSquareBracket). Je m’assure que l’expression a bien été retournée.

Ce qui donne le code suivant :

Méthode ParseArray

function TJsonParser.ParseArray: TJsonArrayExpression;
begin
Result := TJsonArrayExpression.Create;

EatToken(LeftSquareBracket);
if (FCurrentToken.Kind <> RightSquareBracket) then
Result.SetElements(ParseElements);

EatToken(RightSquareBracket);
end;

La syntaxe EBNF de l’expression Elements est rigoureusement identique à celle de l’expression Members. Nous nous passerons de tableaux explicatifs et aboutirons directement au code suivant :

Méthode ParseElements

function TJsonParser.ParseElements: TJsonElementsExpression;
begin
Result := TJsonElementsExpression.Create(ParseValue);

if (FCurrentToken.Kind = Comma) then
begin
EatToken(Comma);
Result.SetElements(ParseElements);
end;
end;

Comme vu précédemment, l’expression Value n’est qu’une liste d’alternatives séparées par des pipes. Son résultat dépendra donc d’une structure Case Of basée sur le jeton trouvé :

- une expression String et l’appel de la méthode ParseString seront déterminés par la présence à ce moment du jeton représentant un guillemet double ;

- une expression Number et l’appel de la méthode ParseNumber sont déterminés par la présence à ce moment d’un jeton représentant le signe négatif, le chiffre 0 ou l’un des chiffres de 1 à 9 ;

- une expression Object et l’appel de la méthode ParseObject seront déterminés par la présence à ce moment du jeton représentant une accolade ouvrante ;

- une expression Array et l’appel de la méthode ParseArray seront déterminés par la présence à ce moment du jeton représentant un crochet ouvrant ;

- une expression True sera déterminée par la présence à ce moment d’un jeton représentant la valeur « true ». Il s’agit d’une expression terminale, ce qui implique qu’il faudra appeler EatToken(TrueValue) dans ce cas ;

- une expression False sera déterminée par la présence à ce moment d’un jeton représentant la valeur « false » . Il s’agit d’une expression terminale, ce qui implique qu’il faudra appeler EatToken(FalseValue) dans ce cas ;

- une expression Null sera déterminée par la présence à ce moment d’un jeton représentant la valeur « null ». Il s’agit d’une expression terminale, ce qui implique qu’il faudra appeler EatToken(NullValue) dans ce cas ;

- tout autre type de jeton représentera un non-respect de la grammaire.

Cela donne le code suivant :

Méthode ParseValue

function TJsonParser.ParseValue: TJsonExpression;
begin
case (FCurrentToken.Kind) of
DoubleQuote : Result := ParseString;
Less, Zero, OneNine : Result := ParseNumber;
LeftCurlyBracket : Result := ParseObject;
LeftSquareBracket : Result := ParseArray;
TrueValue :
begin
Result := TJsonTrueExpression.Create;
EatToken(TrueValue);
end;
FalseValue :
begin
Result := TJsonFalseExpression.Create;
EatToken(FalseValue);
end;
NullValue :
begin
Result := TJsonNullExpression.Create;
EatToken(NullValue);
end;
else
raise TBadTokenException.Create(Value, FCurrentToken.Kind, FCurrentToken.Position);
end;
end;

L’expression String est particulière puisqu’elle peut légitimement contenir des caractères qui serviraient en temps normal de jetons dans la grammaire. Nous pourrions recevoir ces jetons et les considérer comme du simple texte dans le parseur, mais il resterait le problème des espaces blancs que l’analyseur est censé ignorer pour ne pas polluer le parseur avec du bruit.

Il y a deux options :

- ne pas ignorer ces espaces pour que le parseur puisse les obtenir et les transformer en texte brut, avec l’avantage de laisser l’analyseur ignorant de l’inutilité de ces espaces dans la grammaire, mais avec l’inconvénient de devoir complexifier le parseur pour tenir compte des nouveaux jetons dans chacune de ces méthodes, en réduisant la lisibilité du code par la même occasion ;

- dire à l’analyseur qu’il doit momentanément considérer ces espaces (et les autres caractères déterminants par la même occasion) comme du texte, avec l’avantage de simplifier le parseur, mais avec l’inconvénient d’introduire plus de notions de grammaire qu’il n’en faudrait dans l’analyseur, mais en gardant cependant un code plus facile à lire et à comprendre.

Ma règle pour choisir consiste à favoriser l’option dont les inconvénients nuisent le moins à la lisibilité et à la maintenabilité du code, ainsi qu’à la fluidité d’exécution du programme. Ce qui me pousse dans ce cas précis à choisir la deuxième option.

Je vais donc déclarer une énumération pour le mode d’analyse avec pour le moment deux valeurs : une analyse classique et une analyse de texte.

Énumération du mode d’analyse

TJsonTokenizeMode = (Classic, Strings);

Du coup, il faut stocker ce mode d’analyse dans le parseur et le passer à l’analyseur au moment où je lui demande le jeton.

Changement dans le constructeur du parseur

constructor TJsonParser.Create(Tokenizer: TJsonTokenizer);
begin
FTokenizer := Tokenizer;
FTokenizeMode := Classic;
FCurrentToken := FTokenizer.NextToken(FTokenizeMode);
end;

Ne pas oublier la méthode EatToken :

Méthode EatToken

procedure TJsonParser.EatToken(Kind: TJsonTokenKind);
begin
if (FCurrentToken.Kind <> Kind) then
raise TBadTokenException.Create(Kind, FCurrentToken.Kind, FCurrentToken.Position);

FCurrentToken := FTokenizer.NextToken(FTokenizeMode);
end;

Le tableau pour l’expression String est le suivant :

““”

Expression terminale indiquant la présence d’une chaîne de caractères. Il n’y a rien à faire ici.

,

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu et en tenant compte que le prochain jeton devra être analysé comme une chaîne. Passage du mode d’analyse à Strings et appel à EatToken(DoubleQuote).

[

Insertion d’une condition sur la présence des expressions terminales pouvant se trouver au début de l’expression non terminale Characters et ouverture d’un bloc conditionnel dans ce cas. L’expression Characters étant l’unique option et sachant que la première expression terminale la composant peut être n’importe quoi, il est préférable de rechercher l’absence d’un autre guillemet double signifiant la fin de la chaîne.

Characters

Appel à la méthode ParseCharacters et affectation de son résultat à l’expression courante. Cela implique que le résultat soit déjà instancié à ce moment-là ou que ce retour soit stocké jusqu’à l’instanciation du résultat.

]

Fin du bloc conditionnel.

,

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Puisqu’on ne peut pas savoir quel sera ce jeton (règle 2), l’appel à EatToken devra être effectué par la méthode qui connaîtra cette information.

“””

Expression terminale indiquant la fin d’une chaîne de caractères. Cela signifie qu’on doit remettre le mode d’analyse à Classic.

;

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Appel à EatToken(DoubleQuote). Je m’assure que l’expression a bien été retournée.

Le code en résultant est le suivant :

Méthode ParseString

function TJsonParser.ParseString: TJsonStringExpression;
begin
FTokenizeMode := Strings;
EatToken(DoubleQuote);
Result := TJsonStringExpression.Create;

if (FCurrentToken.Kind <> DoubleQuote) then
Result.SetCharacters(ParseChararacters);

FTokenizeMode := Classic;
EatToken(DoubleQuote);
end;

L’expression Characters ne présente rien que nous n’ayons déjà vu. Sa grammaire implique un appel à la méthode ParseSingleCharacter qui portera la responsabilité de l’appel à EatToken ou la déléguera à son tour avant de vérifier la présence (ou dans ce cas l’absence) d’une expression terminale déterminant si on doit faire un appel récursif.

Méthode ParseCharacters

function TJsonParser. ParseChararacters: TJsonCharactersExpression;
var Character : TJsonSingleCharacterExpression;
begin
Character := ParseSingleCharacter;
Result := TJsonCharactersExpression.Create(Character);
If (FCurrentToken.Kind  <> DoubleQuote) then
Result.SetCharacters(ParseChararacters);
end;

L’expression SingleCharacter est un peu plus complexe. Voici le tableau correspondant :

?0020?..?10FFFF? – “”” – “\”

Les séquences spéciales induites par la présence des points d’interrogation indiquent que ces codes alphanumériques représentent quelque chose qui n’a pas pu être explicité autrement dans la grammaire. Elles sont séparées par deux points consécutifs indiquant une plage d’alternatives parmi tous les codes entre et incluant elles deux, mais qui sont représentées par le même jeton.

Vient ensuite un caractère d’exclusion suivi de l’expression terminale à exclure (un guillemet double). Donc le guillemet double ne doit pas être considéré comme jeton du même type que les autres et doit être traité différemment. En effet, il marque la fin de la chaîne de caractères et son jeton est nécessairement différent. Mais il ne peut de toute façon pas être retrouvé ici puisque l’unique appel à ParseSingleCharacter est conditionné par l’absence de ce caractère.

Vient enfin un second caractère d’exclusion lui aussi suivi d’une expression terminale à exclure (l’antislash). Donc l’antislash ne doit pas être considéré comme jeton du même type que les autres et doit être traité différemment. En effet, il marque la présence d’une expression Escape au sein même de la chaîne et son jeton est nécessairement différent.

(*Any character except double quote and backslash*)

Un simple commentaire. Il n’y a rien à faire ici.

|

Le caractère pipe sépare chacune des options d’une structure Case of. Donc tout ce qui se trouve avant ce caractère jusqu’à en trouver un autre, un signe égal ou une virgule (qui ne serait pas incluse entre des parenthèses, des crochets ou des accolades) est une option et tout ce qui se trouve derrière jusqu’à trouver un autre caractère pipe, un point-virgule ou une virgule (qui ne serait pas incluse entre des parenthèses, des crochets ou des accolades) est une option.

Escape

Appel à la méthode ParseEscape et affectation de son résultat à l’expression courante. Cela implique que le résultat soit déjà instancié à ce moment-là ou que ce retour soit stocké jusqu’à l’instanciation du résultat. Escape est placé derrière un caractère pipe et correspond donc à une option. Mais il n’est pas de la responsabilité de ParseSingleCharacter de déterminer quelles sont les différentes options induites. On la délèguera donc à ParseEscape et cela change la structure du Case Of, car l’option ParseEscape devra être mise dans le ELSE.

;

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Puisqu’on ne peut pas savoir quel sera ce jeton (règle 2), l’appel à EatToken devra être effectué par la méthode qui connaîtra cette information. Je m’assure que l’expression a bien été retournée.

Finalement, il n’y a que deux options possibles, autant transformer la structure Case Of en simple bloc conditionnel pour obtenir le code suivant :

Méthode ParseSingleCharacter

function TJsonParser.ParseSingleCharacter: TJsonSingleCharacterExpression;
begin
if (FCurrentToken.Kind = Character) then
begin
Result := TJsonSingleCharacterExpression.Create(FCurrentToken.Value);
EatToken(Character);
end
else
begin
Result := TJsonSingleCharacterExpression(ParseEscape);
end;
end;

L’expression Escape n’est qu’une liste d’alternatives parmi plusieurs expressions terminales et une unique expression non terminale commençant par le jeton Unicode. La valeur à passer dans le constructeur de cette expression sera donc soit le résultat de l’appel à la méthode ParseUnicode (qui sera responsable de la prise en compte de son propre jeton), soit la valeur du jeton courant à partir du moment où il correspond à l’une des expressions terminales de la grammaire et il faudra appeler le EatToken sur le jeton correspondant.

Méthode ParseEscape

function TJsonParser.ParseEscape: TJsonEscapeExpression;
begin
if (FCurrentToken.Kind = Unicode) then
Result := TJsonEscapeExpression.Create(ParseUnicode)
else
begin
Result := TJsonEscapeExpression.Create(FCurrentToken.Value);
case (FCurrentToken.Kind) of
EscapedDoubleQuote : EatToken(EscapedDoubleQuote);
BackSlash : EatToken(BackSlash);
Slash  : EatToken(Slash);
BackSpace : EatToken(BackSpace);
Tab : EatToken(Tab);
NewLine : EatToken(NewLine);
FormFeed : EatToken(FormFeed);
Return : EatToken(Return);
else
FreeAndNil(Result);
raise TBadTokenException.Create(Character, FCurrentToken.Kind, FCurrentToken.Position);
end;
end;
end;

Aucune difficulté pour la méthode ParseUnicode qui se contente de vérifier le jeton Unicode avant d’appeler quatre fois la méthode ParseHexadecimal pour construire l’expression Unicode.

Méthode ParseUnicode

var First, Second, Third, Fourth : TJsonHexadecimalExpression;
begin
EatToken(Unicode);

First := ParseHexadecimal;
Second := ParseHexadecimal;
Third := ParseHexadecimal;
Fourth := ParseHexadecimal;

Result := TJsonUnicodeExpression.Create(First, Second, Third, Fourth);

L’expression Hexadecimal consiste en une simple alternative entre plusieurs plages d’expressions terminales. On se contente de vérifier que la valeur du jeton est bien l’une des expressions terminales imposées avant de passer sa valeur au constructeur.

Méthode ParseHexadecimal

function TJsonParser.ParseHexadecimal: TJsonHexadecimalExpression;
Const
Allowed = ['0','1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F',
'a', 'b', 'c', 'd', 'e', 'f'];

var Hexa : string;
begin
Hexa := FCurrentToken.Value;

if not (Hexa[1] in Allowed) then
raise TStringSyntaxException.Create(Hexa, FCurrentToken.Position);

EatToken(Character);
Result := TJsonHexadecimalExpression.Create(Hexa);
end;

Il n’y a rien que nous n’ayons déjà vu dans l’expression Number et nous aboutissons logiquement au code suivant :

Méthode ParseNumber

function TJsonParser.ParseNumber: TJsonNumberExpression;
begin
Result := TJsonNumberExpression.Create(ParseInteger);

if (FCurrentToken.Kind = Dot) then
Result.SetDecimal(ParseDecimal);

if (FCurrentToken.Kind = Exponent) then
Result.SetExponent(ParseExponent);
end;

Voici le tableau pour l’expression Integer :

[

Insertion d’une condition sur la présence de l’expression terminale qui suit et ouverture d’un bloc conditionnel dans ce cas. Ici il s’agit du signe négatif.

“-”

Signe négatif. Le constructeur de l’expression Integer aura besoin de cette information, je la stocke dans une variable.

]

Fin du bloc conditionnel.

,

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Appel à EatToken(Less). L’appel à EatToken doit être fait à l’intérieur du bloc conditionnel sinon on va déclencher son exception lorsqu’on trouvera un nombre positif.

“0”

Création d’une expression SingleDigit sans paramètre et affectation de son résultat à l’expression courante. Cela implique que le résultat soit déjà instancié à ce moment-là ou que ce retour soit stocké jusqu’à l’instanciation du résultat.

|

Ce caractère pipe indique que la création de SingleDigit est conditionnée par la présence du jeton du zéro. L’autre option est un groupe solidaire commençant par une expression OneNineDigit (1 chiffre de 1 à 9). Avec uniquement deux chemins possibles autant remplacer la structure Case Of par une structure conditionnelle classique.

(

Début d’un groupe solidaire. Cela implique que tous les éléments qui vont suivre doivent faire partie du même bloc conditionnel, induit par le caractère pipe précédent, jusqu’à trouver la parenthèse fermante.

OneNineDigit

Appel à la méthode ParseOneNineDigit et affectation de son résultat à l’expression courante. Cela implique que le résultat soit déjà instancié à ce moment-là ou que ce retour soit stocké jusqu’à l’instanciation du résultat.

,

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Puisqu’on ne peut pas savoir quel sera ce jeton (règle 2), l’appel à EatToken devra être effectué par la méthode qui connaîtra cette information.

[

Insertion d’une condition sur la présence des expressions terminales pouvant se trouver au début de l’expression non terminale Digits et ouverture d’un bloc conditionnel dans ce cas. L’expression Digits Débute par SingleDigit qui débute soit par le chiffre 0, soit par un chiffre de 1 à 9. Nous avons nos conditions d’entrée.

Digits

Appel à la méthode ParseDigits et affectation de son résultat à l’expression courante. Cela implique que le résultat soit déjà instancié à ce moment-là ou que ce retour soit stocké jusqu’à l’instanciation du résultat.

]

Fin du bloc conditionnel.

)

Fin du groupe solidaire.

;

Il faut demander le jeton suivant en vérifiant au passage que le jeton actuel est bien celui attendu. Dans un cas, il s’agira du jeton du zéro via l’appel à EatToken(Zero), dans l’autre ce sera un jeton OneNine, mais l’application de la règle 2 fait que l’appel à EatToken devra être effectué par la méthode ParseOneNine. Idem si on passe dans ParseDigits. Je m’assure que l’expression a bien été retournée.

Voici le code résultant de ce tableau :

Méthode ParseInteger

function TJsonParser.ParseInteger: TJsonIntegerExpression;
var IsNegative : Boolean;
begin
IsNegative := False;
if (FCurrentToken.Kind = Less) then
begin
IsNegative := True;
EatToken(Less);
end;

if (FCurrentToken.Kind = Zero) then
begin
Result := TJsonIntegerExpression.Create(IsNegative, TJsonSingleDigitExpression.Create);
EatToken(Zero);
end
else
begin
Result := TJsonIntegerExpression.Create(IsNegative, ParseOneNineDigit);

if (FCurrentToken.Kind = Zero) or (FCurrentToken.Kind = OneNine) then
Result.SetFollowingDigits(ParseDigits);
end;
end;

Les autres expressions ne contiennent rien que l’on n’ait déjà vu :

Méthode ParseDecimal

function TJsonParser.ParseDecimal: TJsonDecimalExpression;
begin
EatToken(Dot);
Result := TJsonDecimalExpression.Create(ParseDigits);
end;

function TJsonParser.ParseExponent: TJsonExponentExpression;
var IsNegative : Boolean;
begin
EatToken(Exponent);

IsNegative := False;
if (FCurrentToken.Kind = Plus) then
EatToken(Plus)
else if (FCurrentToken.Kind = Less) then
begin
EatToken(Less);
IsNegative := True;
end;

Result := TJsonExponentExpression.Create(ParseDigits, IsNegative);
end;

function TJsonParser.ParseDigits: TJsonDigitsExpression;
var SingleDigit : TJsonSingleDigitExpression;
begin
SingleDigit := ParseSingleDigit;
Result := TJsonDigitsExpression.Create(SingleDigit);

if (FCurrentToken.Kind = Zero) or (FCurrentToken.Kind = OneNine) then
Result.SetFollowingDigits(ParseDigits);
end;

function TJsonParser.ParseSingleDigit: TJsonSingleDigitExpression;
var OneNineDigit : TJsonOneNineDigitExpression;
begin
if (FCurrentToken.Kind = Zero) then
begin
Result := TJsonSingleDigitExpression.Create;
EatToken(Zero);
end
else
begin
OneNineDigit := ParseOneNineDigit;
Result := TJsonSingleDigitExpression.Create(OneNineDigit);
end;
end;

function TJsonParser.ParseOneNineDigit: TJsonOneNineDigitExpression;
begin
Result := TJsonOneNineDigitExpression.Create(FCurrentToken.Value);
EatToken(OneNine);
end;

V-B-3 - L’analyseur syntaxique

L’analyseur syntaxique (souvent appelée par le terme anglais « Tokenizer » ou « Lexer ») est la partie qui transforme le texte brut en jeton.
Il aura donc besoin de demander et de conserver la chaine JSON.
Il aura également besoin de savoir où il en est, et donc de conserver le dernier caractère lu et sa position.

Nous savons qu’il implémentera une méthode NextToken appelée par le parseur pour renvoyer le jeton courant et nous savons que celle-ci aura un paramètre pour spécifier le mode d’analyse.

Finalement la structure de base ressemble à ceci :

Déclaration basique de l’analyseur

TJsonTokenizer = class
private
FJson : String;
FCurrentChar : String;
FPosition : Integer;
protected
public
constructor Create(Const Json : String);

function NextToken(Const Mode : TJsonTokenizeMode) : TJsonToken;
end;

Dans le constructeur, on se contente d’initialiser les membres privés.

Constructeur de l’analyseur

constructor TJsonTokenizer.Create(const Json: String);
begin
FJson := Json;
FPosition := 1;
FCurrentChar := Copy(FJson, 1, 1);
end;

La prochaine étape est une mécanique pour passer au caractère suivant. Déclarons une méthode NextCharacter qui changera la position et récupèrera la chaîne qui s’y trouve. Si on atteint la taille du fichier, cette chaîne sera vide.

Méthode NextCharacter

procedure TJsonTokenizer.NextCharacter;
begin
Inc(FPosition);
if (FPosition > Length(FJson)) then FCurrentChar := ''
else FCurrentChar := Copy(FJson, FPosition, 1);
end;

Reste le vif du sujet, la transformation du texte brut en jeton.
Par défaut, on renvoie le jeton de fin de fichier.

Méthode NextToken (premier pas)

function TJsonTokenizer.NextToken(Const Mode : TJsonTokenizeMode): TJsonToken;
begin
Result := TJsonToken.Create(EndOfFile, '', FPosition);
end;

Ensuite nous allons ignorer les espaces blancs.

Méthode NextToken (ignorer les espaces blancs)

function TJsonTokenizer.NextToken(Const Mode : TJsonTokenizeMode): TJsonToken;
begin
while not(SameText(FCurrentChar, '')) do
begin
if (SameText(FCurrentChar, WHITE_SPACE)) or (SameText(FCurrentChar, Chr(9)))
or (SameText(FCurrentChar, Chr(10))) or (SameText(FCurrentChar, Chr(13))) then
begin
NextCharacter;
Continue;
end;
end;

if (FCurrentChar = '') then
Result := TJsonToken.Create(EndOfFile, '', FPosition);
end;

Le reste est une succession de tests sur le caractère courant pour déterminer le jeton.

Méthode NextToken (détermination des jetons)

function TJsonTokenizer.NextToken(Const Mode : TJsonTokenizeMode): TJsonToken;
begin
Result := nil;
while not(SameText(FCurrentChar, '')) do
begin

if (SameText(FCurrentChar, LEFT_CURLY_BRACKET)) then
begin
Result := TJsonToken.Create(LeftCurlyBracket, LEFT_CURLY_BRACKET, FPosition);
NextCharacter;
Break;
end;

if (SameText(FCurrentChar, RIGHT_CURLY_BRACKET)) then
begin
Result := TJsonToken.Create(RightCurlyBracket, RIGHT_CURLY_BRACKET, FPosition);
NextCharacter;
Break;
end;

if (SameText(FCurrentChar, LEFT_SQUARE_BRACKET)) then
begin
Result := TJsonToken.Create(LeftSquareBracket, LEFT_SQUARE_BRACKET, FPosition);
NextCharacter;
Break;
end;

if (SameText(FCurrentChar, RIGHT_SQUARE_BRACKET)) then
begin
Result := TJsonToken.Create(RightSquareBracket, RIGHT_SQUARE_BRACKET, FPosition);
NextCharacter;
Break;
end;

if (SameText(FCurrentChar, DOUBLE_QUOTE)) then
begin
Result := TJsonToken.Create(DoubleQuote, DOUBLE_QUOTE, FPosition);
NextCharacter;
Break;
end;

if (SameText(FCurrentChar, COLON_CHARACTER)) then
begin
Result := TJsonToken.Create(Colon, COLON_CHARACTER, FPosition);
NextCharacter;
Break;
end;

if (SameText(FCurrentChar, COMMA_CHARACTER)) then
begin
Result := TJsonToken.Create(Comma, COMMA_CHARACTER, FPosition);
NextCharacter;
Break;
end;

if (SameText(FCurrentChar, WHITE_SPACE)) or (SameText(FCurrentChar, Chr(9)))
or (SameText(FCurrentChar, Chr(10))) or (SameText(FCurrentChar, Chr(13))) then
begin
NextCharacter;
Continue;
end;

end;

if (FCurrentChar = '') then
Result := TJsonToken.Create(EndOfFile, '', FPosition);

end;

Vous noterez que pour le moment, je ne tiens pas compte des jetons TrueValue, FalseValue et NullValue, ni des nombres, ni du contenu des chaînes de caractères.

Jusqu’à présent les jetons traités étaient tous contenus dans un unique caractère. Ce n’est pas le cas pour les jetons TrueValue, FalseValue et NullValue. Il faut donc prévoir d’itérer autant de fois que nécessaire pour tenir compte du jeton en entier. Cela va nécessiter une méthode pour voir les caractères qui suivent sans changer la position courante, car cela décalerait tout.

Méthode PeekString

function TJsonTokenizer.PeekString(Count: Integer): String;
begin
Result := Copy(FJson, FPosition, Count);
end;

À partir de là, on peut comparer les chaînes sur n caractères avec les valeurs constantes des expressions true, false, et null. Puis appeler NextCharacter autant de fois que nécessaire pour ne rien décaler. Le code ci-dessous n’est pas complet, je n’ai mis que ce qui concerne les expressions true, false et null et ce qui permet de situer où je rajoute le code.

Méthode NextToken (expressions terminales true, false et null)

function TJsonTokenizer.NextToken(Const Mode : TJsonTokenizeMode): TJsonToken;
var ValueLength : Integer;
i : Integer;
begin
Result := nil;
while not(SameText(FCurrentChar, '')) do
begin

// code before is hidden

ValueLength := Length(TRUE_VALUE);
if (SameText(TRUE_VALUE, PeekString(ValueLength))) then
begin
for i := 1 to ValueLength do
NextCharacter;

Result := TJsonToken.Create(TrueValue, TRUE_VALUE, FPosition);
Break;
end;

ValueLength := Length(FALSE_VALUE);
if (SameText(FALSE_VALUE, PeekString(ValueLength))) then
begin
for i := 1 to ValueLength do
NextCharacter;

Result := TJsonToken.Create(FalseValue, FALSE_VALUE, FPosition);
Break;
end;

ValueLength := Length(NULL_VALUE);
if (SameText(TRUE_VALUE, PeekString(ValueLength))) then
begin
for i := 1 to ValueLength do
NextCharacter;

Result := TJsonToken.Create(NullValue, NULL_VALUE, FPosition);
Break;
end;

if (SameText(FCurrentChar, WHITE_SPACE)) or (SameText(FCurrentChar, Chr(9)))
or (SameText(FCurrentChar, Chr(10))) or (SameText(FCurrentChar, Chr(13))) then
begin
NextCharacter;
Continue;
end;

end;

if (FCurrentChar = '') then
Result := TJsonToken.Create(EndOfFile, '', FPosition);

end;

Pour les chaînes, j’ai un paramètre qui dit que je dois analyser les choses différemment. Je vais tester le mode et appeler une méthode qui fera une analyse différente. Notez que je place ce bout de code au début sinon je vais renvoyer un mauvais jeton si je trouve un caractère structurant dans la chaîne.

Méthode NextToken (gestion des chaînes)

function TJsonTokenizer.NextToken(Const Mode : TJsonTokenizeMode): TJsonToken;
var ValueLength : Integer;
i : Integer;
begin
Result := nil;
while not(SameText(FCurrentChar, '')) do
begin

if (Mode = Strings) then
begin
Result := TokenizeText;
Break;
end;

if (SameText(FCurrentChar, LEFT_CURLY_BRACKET)) then
begin
Result := TJsonToken.Create(LeftCurlyBracket, LEFT_CURLY_BRACKET, FPosition);
NextCharacter;
Break;
end;

// Code below is hidden

end;

if (FCurrentChar = '') then
Result := TJsonToken.Create(EndOfFile, '', FPosition);

end;

Je reviendrai plus tard sur la méthode TokenizeText. Pour le moment, je vais me concentrer sur les nombres. Si le caractère analysé n’est pas dans une chaîne, que ce n’est pas un caractère structurant, ni un espace blanc, ni une expression true, false ou null, alors il s’agit nécessairement d’un nombre. La méthode suivante permet de vérifier cela.

Méthode IsNumber

function TJsonTokenizer.IsNumber(Const Character : String): Boolean;
var Value : Integer;

const
Allowed = ['-','+', 'e', 'E', '.'];
begin
Result := ((TryStrToInt(Character, Value)) or (Character[1] in Allowed));
end;

Cette vérification permet de ne pas à avoir à évaluer chacun des caractères de 1 à 9 individuellement et de simplifier le code comme ceci. Là encore, ce code ne montre que l’essentiel.

Méthode NextToken (gestion des nombres)

function TJsonTokenizer.NextToken(Const Mode : TJsonTokenizeMode): TJsonToken;
begin
Result := nil;
while not(SameText(FCurrentChar, '')) do
begin

// Code above is hidden

if (IsNumber(FCurrentChar)) then
begin
if (SameText(FCurrentChar, EXPONENT_CHARACTER)) then
Result := TJsonToken.Create(Exponent, EXPONENT_CHARACTER, FPosition)
else if (SameText(FCurrentChar, DOT_CHARACTER)) then
Result := TJsonToken.Create(Dot, DOT_CHARACTER, FPosition)
else if (SameText(FCurrentChar, PLUS_SIGN_CHARACTER)) then
Result := TJsonToken.Create(Plus, PLUS_SIGN_CHARACTER, FPosition)
else if (SameText(FCurrentChar, LESS_SIGN_CHARACTER)) then
Result := TJsonToken.Create(Less, LESS_SIGN_CHARACTER, FPosition)
else if (SameText(FCurrentChar, ZERO_CHARACTER)) then
Result := TJsonToken.Create(Zero, ZERO_CHARACTER, FPosition)
else
Result := TJsonToken.Create(OneNine, FCurrentChar, FPosition);

NextCharacter;
Break;
end;

if (SameText(FCurrentChar, WHITE_SPACE)) or (SameText(FCurrentChar, Chr(9)))
or (SameText(FCurrentChar, Chr(10))) or (SameText(FCurrentChar, Chr(13))) then
begin
NextCharacter;
Continue;
end;

end;

if (FCurrentChar = '') then
Result := TJsonToken.Create(EndOfFile, '', FPosition);

end;

La méthode TokenizeText est particulière, car elle a besoin de connaître plus de détails de grammaire qu’il n’est normalement nécessaire (nous en avons vu les raisons dans la partie V.B.2 sur le parseur). Nous allons à présent l’implémenter pas à pas. Et le cas le plus simple consiste en une chaîne vide, qui implique que le caractère courant est le guillemet double de fin de chaîne.

Méthode TokenizeText (chaîne vide)

function TJsonTokenizer.TokenizeText: TJsonToken;
begin
if (SameText(FCurrentChar, DOUBLE_QUOTE)) then
begin
Result := TJsonToken.Create(DoubleQuote, DOUBLE_QUOTE, FPosition);
NextCharacter;
end;
end;

Si la chaîne n’est pas vide, nous pouvons soit avoir n’importe quel caractère sauf le caractère d’échappement et le guillemet double, soit avoir une expression commençant par un caractère d’échappement. Le cas du guillemet double est géré, c’est celui de la chaîne vide. Il faut tester la présence ou l’absence du caractère d’échappement pour savoir dans quel cas on est. Et le cas le plus simple et celui où il est absent.

Méthode TokenizeText (chaîne standard)

function TJsonTokenizer.TokenizeText: TJsonToken;
begin
if (SameText(FCurrentChar, DOUBLE_QUOTE)) then
Result := TJsonToken.Create(DoubleQuote, DOUBLE_QUOTE, FPosition)
else
begin
if not(SameText(FCurrentChar, BACKSLASH_CHARACTER)) then
Result := TJsonToken.Create(Character, FCurrentChar, FPosition)
else
begin

end;
end;

NextCharacter;
end;

Si on retrouve le caractère d’échappement, c’est le caractère d’après qui va déterminer quel jeton renvoyer.

Méthode TokenizeText (complète)

function TJsonTokenizer.TokenizeText: TJsonToken;
var Special  : String;
begin
if (SameText(FCurrentChar, DOUBLE_QUOTE)) then
Result := TJsonToken.Create(DoubleQuote, DOUBLE_QUOTE, FPosition)
else
begin
if not(SameText(FCurrentChar, BACKSLASH_CHARACTER)) then
Result := TJsonToken.Create(Character, FCurrentChar, FPosition)
else
begin
Special := FCurrentChar;

NextCharacter;
Special := Special + FCurrentChar;

if (SameText(FCurrentChar, DOUBLE_QUOTE)) then
Result := TJsonToken.Create(EscapedDoubleQuote, Special, FPosition)
else if (SameText(FCurrentChar, BACKSLASH_CHARACTER)) then
Result := TJsonToken.Create(BackSlash, Special, FPosition)
else if (SameText(Special, SLASH_CHARACTER)) then
Result := TJsonToken.Create(Slash, Special, FPosition)
else if (SameText(Special, BACKSPACE_CHARACTER)) then
Result := TJsonToken.Create(BackSpace, Special, FPosition)
else if (SameText(Special, TAB_CHARACTER)) then
Result := TJsonToken.Create(Tab, Special, FPosition)
else if (SameText(Special, NEW_LINE_CHARACTER)) then
Result := TJsonToken.Create(NewLine, Special, FPosition)
else if (SameText(Special, FORM_FEED_CHARACTER)) then
Result := TJsonToken.Create(FormFeed, Special, FPosition)
else if (SameText(Special, RETURN_CHARACTER)) then
Result := TJsonToken.Create(Return, Special, FPosition)
else if (SameText(Special, UNICODE_CHARACTER)) then
Result := TJsonToken.Create(Unicode, Special, FPosition)
else
Result := TJsonToken.Create(NotManaged, Special, FPosition);
end;
end;

NextCharacter;
end;

Nous en avons terminé avec l’analyseur syntaxique.

V-B-4 - Le code d’appel

Le code suivant va charger un fichier JSON et appeler la mécanique de construction de l’arbre.

Construction calculée de l’arbre depuis un fichier JSON

function GetJsonExpressionTreeFromFile(Const Filename : String) : TJsonExpression;
var List : TStrings;
Json : String;
Tokenizer : TJsonTokenizer;
Parser : TJsonParser;
begin
List := TStringList.Create;
try
List.LoadFromFile(Filename);
Json := List.Text;
finally
FreeAndNil(List);
end;

try
Tokenizer := TJsonTokenizer.Create(Json);
Parser := TJsonParser.Create(Tokenizer);
try
Result := Parser.Parse;
finally
FreeAndNil(Parser);
FreeAndNil(Tokenizer);
end;
except
on E:Exception do
begin
Writeln(E.Message);
end;
end;
end;

VI - Interprétation

Suivant la grammaire et/ou le domaine d’application, le travail d’interprétation peut prendre divers degrés de complexité, nécessiter ou non un contexte.

VI-A - Contrat du contexte

Pour JSON, un contexte évident (contexte 1) consisterait à désérialiser un flux, mais on peut facilement en imaginer d’autres :

- construire un flux JSON (contexte 2) puisque l’arbre n’est pas forcément calculé à partir d’un JSON même si c’est le cas ici ;

- déduire une structure de classes (contexte 3) ;

- transformer du JSON en XML (contexte 4).

Cela donne au contexte une responsabilité plus importante que le simple stockage d’information. Il prend de l’intelligence et traduit l’interprétation en autre chose.

Bien évidemment, le contrat du contexte dépend de la façon dont l’arbre d’expressions est parcouru, donc il dépend aussi de la grammaire. Et encore une fois, ce sont généralement les expressions terminales qui sont déterminantes.

Prenons par exemple l’expression Object et imaginons ce qu’il conviendrait de faire dans les différents contextes lors de la rencontre avec les expressions terminales qui la composent :

- l’accolade ouvrante :

- contexte 1 : instancier un objet. Cet objet est soit l’objet racine si c’est la première accolade ouvrante rencontrée, soit un objet dont le nom de la classe a été déduit à partir d’une expression String parcourue avant,

- contexte 2 : écriture d’une accolade ouvrante à la suite du flux existant,

- contexte 3 : créer une définition de classe. Il faudra certainement prévoir un moyen pour revenir dessus, car nous devrons potentiellement y revenir pour y ajouter une propriété si on passe plus loin dans l’expression Pair via l’expression Members,

- contexte 4 : écrire un nœud ouvrant dont le nom est soit « Root » (ou autre nom plus approprié), soit un nom déduit à partir d’une expression String parcourue avant ;

- l’accolade fermante :

- contexte 1 : retourner l’objet,

- contexte 2 : écrire une accolade fermante à la suite du flux existant,

- contexte 3 : considérer la définition de la classe comme complète,

- contexte 4 : écrire un nœud fermant pour le nœud crée lorsqu’on a trouvé l’accolade ouvrante allant de pair avec cette accolade fermante.

Voici le même raisonnement avec l’expression Members :

- La virgule indiquant la présence d’une autre expression Members :

- contexte 1 : à première vue, il n’y a rien à faire car le nom et la valeur de la propriété ont été déduits dans le contexte de l’expression Pair,

- contexte 2 : écriture d’une virgule à la suite du flux existant,

- contexte 3 : à première vue, il n’y a rien à faire car le nom et le type de la propriété ont été déduits dans le contexte de l’expression Pair,

- contexte 4 : à première vue, il n’y a rien à faire car le nœud et son contenu ont été inscrits dans le contexte de l’expression Pair.

Et ainsi de suite jusqu’à l’expression Null.

Finalement, le contrat du contexte JSON va ressembler à ceci :

Interface IJsonContext

IJsonContext = interface
['{210BA2C9-0EEE-423F-85C1-61E101EB4CC6}']
procedure EnterInObject(ObjectExpression : TJsonObjectExpression);  // '{'
procedure EnterInPair(PairExpression : TJsonPairExpression); // '"'
procedure EnterInColon(PairExpression : TJsonPairExpression); // ':'
procedure EnterInMembersComma(MembersExpression : TJsonMembersExpression); // ','
procedure EnterInArray(ArrayExpression : TJsonArrayExpression); // '['
procedure EnterInElementsComma(ElementsExpression : TJsonElementsExpression); // Comma => ','
procedure EnterInStringValue(StringExpression : TJsonStringExpression); // '"'
procedure EnterInNumberValue(NumberExpression : TJsonNumberExpression); // '-', '0', '1'..'9'
procedure EnterInTrueValue(TrueExpression : TJsonTrueExpression); // 'true'
procedure EnterInFalseValue(FalseExpression : TJsonFalseExpression); // 'false'
procedure EnterInNullValue(NullExpression : TJsonNullExpression); // 'null'

procedure ExitFromObject(ObjectExpression : TJsonObjectExpression); // '}'
procedure ExitFromArray(ArrayExpression : TJsonArrayExpression); // ']'

function ComputeTransformations : String; overload;
function ComputeTransformations(ClassType : TClass) : TObject; overload;
end;

Notez que chacune des méthodes prend en paramètre une expression et que cette expression n’est pas la même à chaque fois. Cela implique que le type de l’expression dépend de l’endroit où elle est appelée, et c’est logique puisque je ne vais pas dire au contexte que je rencontre une autre expression que celle effectivement rencontrée.

Ces expressions sont en paramètre parce que je ne sais pas si le consommateur du contrat en aura besoin ou non. Donc je pars du principe qu’il en aura besoin même si je n’en vois pas la raison au moment où je décris le contrat.

VI-B - Appel au contexte

L’étape d’après consiste à appeler les méthodes définies par le contrat du contexte au sein des méthodes d’interprétation des différentes expressions en respectant l’ordre imposé par la grammaire.

Pour l’expression Object, on retrouve l’accolade ouvrante au tout début et l’accolade fermante à la toute fin.
L’appel à EnterInObject doit donc être fait au début et l’appel à ExitFromObject doit être fait à la fin.

Méthode d’interprétation de l’expression Object

procedure TJsonObjectExpression.Interpret(Context : IJsonContext);
begin
Writeln('Object');
Context.EnterInObject(Self);

if (Assigned(FMembers)) then
FMembers.Interpret(Context);

Context.ExitFromObject(Self);
end;

Pour l’expression Members, la virgule est utilisée uniquement s’il y a une autre expression Members, donc on appellera EnterInMembersComma uniquement si l’expression dans FMembers est assignée.

Méthode d’interprétation de l’expression Members

procedure TJsonMembersExpression.Interpret(Context : IJsonContext);
begin
Writeln('Members');

FPair.Interpret(Context);
if (Assigned(FMembers)) then
begin
Context.EnterInMembersComma(Self);
FMembers.Interpret(Context);
end;
end;

Et ceci, jusqu’à l’expression Null.

Méthodes d’interprétations

procedure TJsonPairExpression.Interpret(Context : IJsonContext);
begin
Context.EnterInPair(Self);
FName.Interpret(Context);
Writeln('Pair, Identifier = ' + FName.Value);
Context.EnterInColon(Self);
FValue.Interpret(Context);
end;

procedure TJsonArrayExpression.Interpret(Context : IJsonContext);
begin
Writeln('Array');
Context.EnterInArray(Self);

if (Assigned(FElements)) then
FElements.Interpret(Context);

Context.ExitFromArray(Self);
end;

procedure TJsonElementsExpression.Interpret(Context : IJsonContext);
begin
Writeln('Elements');

FValue.Interpret(Context);
if (Assigned(FElements)) then
begin
Context.EnterInElementsComma(Self);
FElements.Interpret(Context);
end;
end;

procedure TJsonNumberExpression.Interpret(Context : IJsonContext);
begin
FStringValue := FInteger.Interpret;

if (Assigned(FDecimal)) then
FStringValue := FStringValue + FDecimal.Interpret;

if (Assigned(FExponent)) then
FStringValue := FStringValue + FExponent.Interpret;

WriteLn('Number, Value = ' + FStringValue);
Context.EnterInNumberValue(Self);
end;

procedure TJsonStringExpression.Interpret(Context : IJsonContext);
begin
if (Assigned(FCharacters)) then
FStringValue := FCharacters.Interpret;

WriteLn('String, Value = ' + FStringValue);
Context.EnterInStringValue(Self);
end;

procedure TJsonTrueExpression.Interpret(Context : IJsonContext);
begin
WriteLn('True');
Context.EnterInTrueValue(Self);
end;

procedure TJsonFalseExpression.Interpret(Context : IJsonContext);
begin
WriteLn('False');
Context.EnterInFalseValue(Self);
end;

procedure TJsonNullExpression.Interpret(Context : IJsonContext);
begin
WriteLn('Null');
Context.EnterInNullValue(Self);
end;

VI-C - Comportement du contexte

Maintenant que nous avons un contrat et des appels aux méthodes de ce contrat pendant l’interprétation, il reste à implémenter le comportement de ce contrat dans les différents contextes.

Nous laisserons de côté le premier contexte qui nécessite des connaissances qui sortent du cadre de ce document et nous concentrerons sur le deuxième qui est beaucoup plus simple.

Et en premier lieu, nous allons créer une classe dont le nom indique ce qu’elle va faire et qui implémente l’interface du contrat.

Classe de contexte dont l’objectif est de produire du JSON

TJsonTreeToJsonStringContext = class(TInterfacedObject, IJSonContext)
private
public
procedure EnterInObject(ObjectExpression : TJsonObjectExpression);
procedure EnterInPair(PairExpression : TJsonPairExpression);
procedure EnterInColon(PairExpression : TJsonPairExpression);
procedure EnterInMembersComma(MembersExpression : TJsonMembersExpression);
procedure EnterInArray(ArrayExpression : TJsonArrayExpression);
procedure EnterInElementsComma(ElementsExpression : TJsonElementsExpression);
procedure EnterInStringValue(StringExpression : TJsonStringExpression);
procedure EnterInNumberValue(NumberExpression : TJsonNumberExpression);
procedure EnterInTrueValue(TrueExpression : TJsonTrueExpression);
procedure EnterInFalseValue(FalseExpression : TJsonFalseExpression);
procedure EnterInNullValue(NullExpression : TJsonNullExpression);

procedure ExitFromObject(ObjectExpression : TJsonObjectExpression);
procedure ExitFromArray(ArrayExpression : TJsonArrayExpression);

function ComputeTransformations : String; overload;
function ComputeTransformations(ClassType : TClass) : TObject; overload;

constructor Create;
end;

Puisque le retour sera du JSON et qu’il sera construit petit à petit pendant le parcours de l’arbre d’expressions, il faut un membre privé de type chaîne de caractères pour stocker l’état du résultat à un instant T. Aussi, nous partons du principe que ne savons pas à partir de quoi a été élaboré l’arbre d’expression. Ce qui implique un traitement particulier pour transformer certaines constantes chaînes de caractères propres à Delphi en quelque chose de valide au niveau du JSON.

Classe de contexte dont l’objectif est de produire du JSON

TJsonTreeToJsonStringContext = class(TInterfacedObject, IJSonContext)
private
FString : String;

function FormatJsonString(Const Value : String) : String;
public
procedure EnterInObject(ObjectExpression : TJsonObjectExpression);
procedure EnterInPair(PairExpression : TJsonPairExpression);
procedure EnterInColon(PairExpression : TJsonPairExpression);
procedure EnterInMembersComma(MembersExpression : TJsonMembersExpression);
procedure EnterInArray(ArrayExpression : TJsonArrayExpression);
procedure EnterInElementsComma(ElementsExpression : TJsonElementsExpression);
procedure EnterInStringValue(StringExpression : TJsonStringExpression);
procedure EnterInNumberValue(NumberExpression : TJsonNumberExpression);
procedure EnterInTrueValue(TrueExpression : TJsonTrueExpression);
procedure EnterInFalseValue(FalseExpression : TJsonFalseExpression);
procedure EnterInNullValue(NullExpression : TJsonNullExpression);

procedure ExitFromObject(ObjectExpression : TJsonObjectExpression);
procedure ExitFromArray(ArrayExpression : TJsonArrayExpression);

function ComputeTransformations : String; overload;
function ComputeTransformations(ClassType : TClass) : TObject; overload;

constructor Create;
end;

Il faut transformer les constantes #8, #9, #10, #12 et #13 en leur équivalent JSON et transcrire les caractères étendus en notations Unicode acceptées par JSON. Les autres caractères pourront être transmis tels quels.

Méthode de transformation des chaînes

function TJsonTreeToJsonStringContext.FormatJsonString(const Value: String): String;
var i, n : Integer;
InternalValue : PWideChar;
Current : WideChar;

Const HexadecimalChars : PWideChar = '0123456789abcdef';

type

TByteChar = record
case integer of
0: (a, b: Byte);
1: (c: WideChar);
end;

begin
Result := '';
InternalValue := PWideChar(WideString(Value));
n := Length(InternalValue) - 1;
for i := 0 to n do
begin
Current := InternalValue[i];
if (SameText(Current, #8)) then
Result := Result + '\b'
else if (SameText(Current, #9)) then
Result := Result + '\t'
else if (SameText(Current, #10)) then
Result := Result + '\n'
else if (SameText(Current, #12)) then
Result := Result + '\f'
else if (SameText(Current, #13)) then
Result := Result + '\r'
else if (Word(Current) > 255) then
begin
Result := Result + '\u'
+ HexadecimalChars[TByteChar(Current).b shr 4]
+ HexadecimalChars[TByteChar(Current).b and $f]
+ HexadecimalChars[TByteChar(Current).a shr 4]
+ HexadecimalChars[TByteChar(Current).a and $f];
end
else if (Current < #32) or (Current > #127) then
begin
Result := Result + '\u00'
+ HexadecimalChars[Ord(Current) shr 4]
+ HexadecimalChars[Ord(Current) and $f]
end
else
begin
Result := Result + Current;
end;
end;
end;

Les méthodes du contrat permettent de placer le bon caractère déterminant au bon endroit.

Placement des caractères déterminants

{ TJsonTreeToJsonStringContext }

constructor TJsonTreeToJsonStringContext.Create;
begin
FString := '';
end;

procedure TJsonTreeToJsonStringContext.EnterInObject(ObjectExpression: TJsonObjectExpression);
begin
FString := FString + '{';
end;

procedure TJsonTreeToJsonStringContext.EnterInPair(PairExpression: TJsonPairExpression);
begin
// Nothing to do here
end;

procedure TJsonTreeToJsonStringContext.EnterInColon(PairExpression : TJsonPairExpression);
begin
FString := FString + ':';
end;

procedure TJsonTreeToJsonStringContext.EnterInMembersComma(MembersExpression : TJsonMembersExpression);
begin
FString := FString + ',';
end;

procedure TJsonTreeToJsonStringContext.EnterInArray(ArrayExpression: TJsonArrayExpression);
begin
FString := FString + '[';
end;

procedure TJsonTreeToJsonStringContext.EnterInElementsComma(ElementsExpression : TJsonElementsExpression);
begin
FString := FString + ',';
end;

procedure TJsonTreeToJsonStringContext.EnterInStringValue(StringExpression: TJsonStringExpression);
begin
FString := FString + '"' + FormatJsonString(StringExpression.Value) + '"';
end;

procedure TJsonTreeToJsonStringContext.EnterInNumberValue(NumberExpression: TJsonNumberExpression);
begin
FString := FString + NumberExpression.Value;
end;

procedure TJsonTreeToJsonStringContext.EnterInTrueValue(TrueExpression: TJsonTrueExpression);
begin
FString := FString + 'true';
end;

procedure TJsonTreeToJsonStringContext.EnterInFalseValue(FalseExpression: TJsonFalseExpression);
begin
FString := FString + 'false';
end;

procedure TJsonTreeToJsonStringContext.EnterInNullValue(NullExpression: TJsonNullExpression);
begin
FString := FString + 'null';
end;

procedure TJsonTreeToJsonStringContext.ExitFromObject(ObjectExpression: TJsonObjectExpression);
begin
FString := FString + '}';
end;

procedure TJsonTreeToJsonStringContext.ExitFromArray(ArrayExpression: TJsonArrayExpression);
begin
FString := FString + ']';
end;

À la fin, il suffit de s’assurer que la chaîne de résultat n’est pas vide, mais contient au moins un objet JSON (qui lui peut être vide).

Obtention du résultat

function TJsonTreeToJsonStringContext.ComputeTransformations: String;
begin
if (SameText(FString, '')) then
FString := '{}';

Result := FString;
end;

Les implémentations des contextes 3 et 4 ne sont pas décrites dans ce document. Si toutefois, elles vous intéressent, vous trouverez le code complet sur ma page.

VII - Avantages / Inconvénients du patron

Comme expliqué au début du document, ce patron repose sur la grammaire. Ce qui peut à la fois être un avantage et un inconvénient.

VII-A - Avantages

Avec de nombreuses expressions, il est aisé de changer et d’étendre la grammaire. Le patron utilisant des classes pour représenter les règles de grammaire, l’extension des fonctionnalités se fait naturellement via l’extension des classes.

Les classes définissant les branches dans l’arbre d’expressions, elles sont faciles à écrire, à générer à partir d’un outil.

Ce patron est largement utilisé dans les compilateurs implémentés avec des langages orientés objet. En JavaScript, il existe de nombreux analyseurs basés sur ce concept. Tous utilisent l’arbre d’expressions pour gérer l’analyse syntaxique (EsLint, StyleLint, Babel).

VII-B - Inconvénients

Ce patron est très utile pour des langages simples où la performance n’est pas le plus important. Au fur et à mesure que la grammaire se complexifie, le nombre d’expressions peut rapidement devenir important et la hiérarchie des classes peut devenir trop complexe pour être maintenue efficacement.

Lorsque la grammaire est très complexe, vous aurez probablement besoin d’une solution plus complexe également comme un générateur de compilateur avec lequel vous n’auriez pas à maintenir un grand nombre de classes par vous-même.

VIII - Liens

EBNF :https://fr.wikipedia.org/wiki/Extended_Backus-Naur_Form

EBNF selon la syntaxe McKeeman : https://www.crockford.com/mckeeman.html

JSON : http://www.json.org

Analyses LL et LR démystifiées : http://blog.reverberate.org/2013/07/ll-and-lr-parsing-demystified.html

LL et LR dans le contexte : Pourquoi les outils d’analyses sont compliqués : http://blog.reverberate.org/2013/09/ll-and-lr-in-context-why-parsing-tools.html

IX - Remerciements

Je tiens à remercier SergioMaster, Alcatîz et ClaudeLELOUP pour leurs remarques ainsi que pour le temps qu’ils ont passé à me relire et à apporter des corrections.