Ajout classe parseur JSON + script de tests.

This commit is contained in:
Fabrice PENHOËT 2021-10-06 16:54:20 +02:00
parent d7d9885b2b
commit 7eadd29be2
7 changed files with 358 additions and 11 deletions

View File

@ -1,6 +1,6 @@
{
"name": "freedatas2html",
"version": "0.6.1",
"version": "0.7.0",
"description": "Visualization of data from various sources (CSV, API, HTML...) with filters, classification, pagination, etc.",
"main": "index.js",
"scripts": {

View File

@ -16,6 +16,8 @@ module.exports =
parserNeedDatas: "Merci de fournir une chaîne de caractères valide à parser.",
parserNeedSource: "Merci de fournir une chaîne de caractères où une url pour les données à parser.",
parserNeedUrl: "Merci de fournir une url valide pour la source distante de données.",
parserRemoteFail: "Erreur rencontrée durant l'accès aux données distantes.",
parserTypeError: "Une donnée a été trouvée avec un type imprévu : ",
renderNeedDatas: "Il ne peut y avoir de pagination, si les données n'ont pas été récupérées.",
renderUnknownField: "Un champ non attendu a été trouvé dans les données à afficher : ",
selector2HTMLFail: "Le création d'un filtre dans le DOM nécessite l'initialisation de l'élément HTML et du numéro du champs à filter.",

View File

@ -4,6 +4,7 @@ const errors=require("./errors.js");
import { Counter, Datas, DatasRenders, DOMElement, Paginations, Parsers, ParseErrors, RemoteSource, Selectors, SortingFields, SortingFunctions } from "./freeDatas2HTMLInterfaces";
import { Pagination} from "./freeDatas2HTMLPagination";
import { ParserForCSV} from "./freeDatas2HTMLParserForCSV";
import { ParserForJSON} from "./freeDatas2HTMLParserForJSON";
import { Render} from "./freeDatas2HTMLRender";
import { Selector } from "./freeDatas2HTMLSelector";
import { SortingField } from "./freeDatas2HTMLSortingField";
@ -29,7 +30,7 @@ export class FreeDatas2HTML
// Le nom des champs trouvés dans les données :
public fields: string[]|undefined=undefined;
// Les données à proprement parler :
public datas: []=[];
public datas: {[index: string]:any}[]=[];
// Les erreurs rencontrées durant le traitement des données reçues :
public parseErrors: ParseErrors[]|undefined;
// Doit-on tout arrêter si une erreur est rencontrée durant le traitement ?
@ -62,8 +63,7 @@ export class FreeDatas2HTML
console.error("Appeler le parseur HTML");
break;
case "JSON":
this.parser=new ParserForCSV();
console.error("Appeler le parseur JSON");
this.parser=new ParserForJSON();
break;
}
if(datas2Parse !== "")

View File

@ -50,7 +50,7 @@ export interface Datas
{
[key: string]: string;
}
export interface ParseErrors // erreurs non bloquantes rencontrées lors du parsage de données
export interface ParseErrors
{
code?: string;
message: string;
@ -59,7 +59,7 @@ export interface ParseErrors // erreurs non bloquantes rencontrées lors du pars
}
export interface ParseResults
{
datas: [];
datas: {[index: string]:string}[];
errors: ParseErrors[];
fields: string[];
}
@ -73,7 +73,7 @@ export interface Parsers
export interface RemoteSource
{
url: string;
headers?: { key:string, value:string|boolean|number }[] ;// revoir types possibles pour Headers ?
headers?: { key:string, value:string }[] ;
withCredentials?:boolean;
}
export interface Selectors

View File

@ -0,0 +1,154 @@
const errors = require("./errors.js");
import { ParseErrors, ParseResults, Parsers, RemoteSource } from "./freeDatas2HTMLInterfaces";
export class ParserForJSON implements Parsers
{
private _datasRemoteSource: RemoteSource={ url:"" };
private _datas2Parse: string="";
private _parseResults: ParseResults|undefined=undefined;
// Revoir tous les protocoles possibles pour une source distante (http(s)://.. , ftp..)
set datasRemoteSource(source: RemoteSource)
{
if(source.url.trim().length === 0)
throw new Error(errors.parserNeedUrl);
else
{
source.url=source.url.trim();
this._datasRemoteSource=source;
}
}
get datasRemoteSource() : RemoteSource
{
return this._datasRemoteSource;
}
set datas2Parse(datas: string)
{
if(datas.trim().length === 0)
throw new Error(errors.parserNeedDatas);
else
this._datas2Parse=datas.trim();
}
get datas2Parse() : string
{
return this._datas2Parse;
}
get parseResults() : ParseResults|undefined
{
return this._parseResults;
}
// Refuse les champs qui ne sont pas des chaînes de caractères
// trim() les autres
public static trimAllFields(fields: any[]) : string[]
{
const nb=fields.length, goodFields: string[]=[];
for(let i=0; i < nb; i++)
{
if(typeof fields[i] === "string")
goodFields.push(fields[i].trim());
}
return goodFields;
}
// async dans le cas d'une source distante
public async parse(): Promise<any>
{
const parser=this;
let parseContent="";
if(parser._datasRemoteSource.url !== "")
{
const headers=new Headers();
if(parser._datasRemoteSource.headers !== undefined)
{
for(let header of parser._datasRemoteSource.headers)
headers.append(header.key, header.value);
}
const credentials : RequestCredentials|undefined=(parser._datasRemoteSource.withCredentials) ? "include" : "omit";
const settings={
method: "GET",
headers: headers,
credentials: credentials,
};
const response=await fetch(parser._datasRemoteSource.url, settings);
if (! response.ok)
throw new Error(errors.parserRemoteFail);
parseContent=await response.text(); // doit en fait retourner du JSON, mais il est parsé plus loin.
}
else if(parser._datas2Parse !== "")
parseContent=parser._datas2Parse;
else
throw new Error(errors.parserNeedSource);
try
{
const datasParsed=JSON.parse(parseContent);
const typesOkForValue=["boolean","number","string"];
let fields: string[]=[], datas: {[index: string]:string}[]=[], parseErrors: ParseErrors[]=[];
// Je peux recevoir 2 tableaux contenant respectivement la liste de champs : string[] + celle des données : any[][]
if(datasParsed.fields !== undefined && Array.isArray(datasParsed.fields) && datasParsed.datas !== undefined && Array.isArray(datasParsed.datas))
{
fields=ParserForJSON.trimAllFields(datasParsed.fields);
const nbFields=fields.length, nbDatas=datasParsed.datas.length;
for(let i=0; i < nbDatas; i++)
{
const dataObject: {[index: string]: string} = {}, nbObjFields=datasParsed.datas[i].length;
for(let j=0; j < nbObjFields && j < nbFields; j++)
{
if(typesOkForValue.indexOf(typeof datasParsed.datas[i][j]) === -1)
parseErrors.push({ row:i, message:errors.parserTypeError+typeof datasParsed.datas[i][j]});
else
dataObject[fields[j]]=datasParsed.datas[i][j]+""; // force le type String
}
if(Object.keys(dataObject).length !== 0)
datas.push(dataObject);
}
}
else // Ou un tableau d'objets {}[], dont les attributs sont les noms des champs
{
let i=0;
for(let data of datasParsed)
{
// Ici les champs sont découverts au fur et à mesure,
// leur ordre peut être différent d'une ligne à l'autre
// et tous les champs ne sont pas systématiquement présents
let dataObject: {[index: string]: string} = {}
for(let field in data)
{
if(typesOkForValue.indexOf(typeof data[field]) !== -1)
{
field=field.trim();
if(field !== "" && fields.indexOf(field) === -1)
fields.push(field);
dataObject[field]=data[field]+""; // force le type String
}
else
parseErrors.push({ row:i, message:errors.parserTypeError+typeof data[field]});
}
if(Object.keys(dataObject).length !== 0)
datas.push(dataObject);
i++;
}
}
if(fields.length === 0) // possible si données fournies non correctement formées.
throw new Error(errors.parserFail);
// datas et errors peuvent par contre rester vides.
parser._parseResults =
{
datas: datas,
errors: parseErrors,
fields: fields,
};
}
catch(e)
{
console.error(e);
throw new Error(errors.parserFail);
}
}
}

190
tests/parserForJSONSpec.ts Normal file
View File

@ -0,0 +1,190 @@
import { RemoteSource } from "../src/freeDatas2HTMLInterfaces";
import { ParserForJSON as Parser } from "../src/freeDatas2HTMLParserForJSON";
const errors=require("../src/errors.js");
describe("Tests du parseur de JSON", () =>
{
let parser: Parser;
beforeEach( () =>
{
parser=new Parser();
});
it("Doit avoir créé une instance du Parser", () =>
{
expect(parser).toBeInstanceOf(Parser);
});
it("Doit générer une erreur si la chaîne de données à parser est vide.", () =>
{
expect(() => { return parser.datas2Parse= "" }).toThrowError(errors.parserNeedDatas);
expect(() => { return parser.datas2Parse= " " }).toThrowError(errors.parserNeedDatas);
});
it("Doit accepter toute chaîne de caractères non vide pour les données à parser.", () =>
{
parser. datas2Parse="datas";
expect(parser.datas2Parse).toEqual("datas");
});
it("Doit générer une erreur si le parseur est lancé sans source de données fournie.", async () =>
{
await expectAsync(parser.parse()).toBeRejectedWith(new Error(errors.parserNeedSource));
});
it("Si le parseur a été appelé avec des données correctes, des résultats doivent être enregistrés.", async () =>
{
parser.datas2Parse= `[{ "nom": "dugenoux"},{ "nom": "dupont"}]`;
await parser.parse();
expect(parser.parseResults).not.toBeUndefined();
});
describe("Accès à des données distantes.", () =>
{
it("Doit générer une erreur si l'url fournie pour importer les données est une chaîne vide.", () =>
{
expect(() => { return parser.datasRemoteSource= { url:"" } }).toThrowError(errors.parserNeedUrl);
expect(() => { return parser.datasRemoteSource= { url:" " } }).toThrowError(errors.parserNeedUrl);
});
it("Doit accepter des paramètres valides pour la source de données distante.", () =>
{
let myRemoteSource: RemoteSource={ url:"zz" };
parser.datasRemoteSource=myRemoteSource;
expect(parser.datasRemoteSource).toEqual(myRemoteSource);
myRemoteSource={ url:"zz", headers: [ { key:"test", value: "coucou"}, { key:"test2", value:"coucou2"}], withCredentials:true };
parser.datasRemoteSource=myRemoteSource;
expect(parser.datasRemoteSource).toEqual(myRemoteSource);
});
it("Si des options sont fournies pour appeler une ressource distante, elles doivent être prises en compte.", async () =>
{
spyOn(window,"fetch").and.callThrough();
parser.datasRemoteSource={ url: "http://localhost:9876/datas/posts.json", withCredentials:true, headers: [{ key:"Authorization", value:"Token YWxhZGRpbjpvcGVuc2VzYW1l" }]};
await parser.parse();
const headers=new Headers();
headers.append("Authorization", "Token YWxhZGRpbjpvcGVuc2VzYW1l");
const credentials : RequestCredentials|undefined="include" ;
const settings={
method: "GET",
headers: headers,
credentials: credentials,
};
expect(fetch).toHaveBeenCalledWith("http://localhost:9876/datas/posts.json", settings);
});
it("Doit générer une erreur, si l'accès aux données distantes est défaillant.", async () =>
{
parser.datasRemoteSource={ url:"http://localhost:9876/datas/posts.jso" }; // une seule lettre vous manque...
await expectAsync(parser.parse()).toBeRejectedWith(new Error(errors.parserRemoteFail));
});
it("Si le parseur a été appelé avec une url fournissant des données correctes, des résultats doivent être enregistrés.", async () =>
{
parser.datasRemoteSource={ url:"http://localhost:9876/datas/posts.json" };
await parser.parse();
expect(parser.parseResults).not.toBeUndefined();
});
});
describe("Noms des champs et données fournies dans deux tableaux distincts.", () =>
{
it("Les valeurs fournies pour les champs doivent être des chaînes de caractères.", () =>
{
const fields=["nom",24,"prénom", true,{ field:"champ"},"âge",["je suis un nom de champ"]];
expect(Parser.trimAllFields(fields)).toEqual(["nom","prénom", "âge"]);
});
it("Les espaces entourant les noms de champs doivent être supprimés.", () =>
{
const fields=[" nom","prénom ", " âge "];
expect(Parser.trimAllFields(fields)).toEqual(["nom","prénom", "âge"]);
});
it("Si des champs en trop sont trouvés dans une ligne de données, ils doivent être ignorés. Idem pour les champs absents.", async () =>
{
parser.datas2Parse=`{ "fields": ["nom","prénom", "âge"], "datas": [["dugenoux","henri",25,"je me champ en trop"],["michu","mariette"]] }`;
await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]);
expect(parser.parseResults.datas).toEqual([{nom:"dugenoux","prénom":"henri", "âge":"25"}, {nom:"michu","prénom":"mariette"}]);
});
it("Si certaines des données fournies ont un type non accepté, elles doivent être ignorées et les erreurs doivent être reportées.", async () =>
{
parser.datas2Parse=`{ "fields": ["nom","prénom", "âge"], "datas": [["dugenoux",{ "prenom":"henri"},25],["michu","mariette",null]] }`;
await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]);
expect(parser.parseResults.datas).toEqual([{nom:"dugenoux", "âge":"25"}, {nom:"michu","prénom":"mariette"}]);
expect(parser.parseResults.errors[0]).toEqual({row:0,message:errors.parserTypeError+"object"});
expect(parser.parseResults.errors[1]).toEqual({row:1,message:errors.parserTypeError+"object"});
});
it("Un enregistrement n'ayant aucune donnée valide sera ignoré.", async () =>
{
parser.datas2Parse=`{ "fields": ["nom","prénom", "âge"], "datas": [["dugenoux","henri",25],[null,{ "prenom":"mariette"},[58]]] }`;
await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]);
expect(parser.parseResults.datas).toEqual([{nom:"dugenoux","prénom":"henri", "âge":"25"}]);
});
it("Si toutes les données fournies sont ok, on doit les retrouver en résultat.", async () =>
{
parser.datas2Parse=`{ "fields": ["nom","prénom", "âge"], "datas": [["dugenoux","henri",25],["michu","mariette",58]] }`;
await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]);
expect(parser.parseResults.datas).toEqual([{nom:"dugenoux","prénom":"henri", "âge":"25"}, {nom:"michu","prénom":"mariette", "âge":"58"}]);
});
});
describe("Données fournies sous forme de tableau d'objets.", () =>
{
it("Les espaces entourant les noms de champs doivent être supprimés.", async () =>
{
parser.datas2Parse=`[{"nom ":"dugenoux"," prénom":"henri"," âge ":25},{"nom":"michu","prénom":"mariette","âge":58}]`;
await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]);
});
it("Si certaines des données fournies ont un type non accepté, elles doivent être ignorées ainsi que leur attribut. Et les erreurs doivent être reportées.", async () =>
{
parser.datas2Parse=`[{"nom":"dugenoux","prénom":{"value":"henri"},"âge":25},{"âge":"58","nom":"michu","prénom":"mariette","pseudo":["madame Michu"]}]`;
await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom", "âge", "prénom"]);
expect(parser.parseResults.datas).toEqual([{nom:"dugenoux", "âge":"25"}, {"âge":"58", nom:"michu", "prénom":"mariette"}]);
expect(parser.parseResults.errors[0]).toEqual({row:0,message:errors.parserTypeError+"object"});
expect(parser.parseResults.errors[1]).toEqual({row:1,message:errors.parserTypeError+"object"});
});
it("Un enregistrement n'ayant aucune donnée valide doit être ignoré.", async () =>
{
parser.datas2Parse=`[{"nom":["dugenoux"],"prénom":{"value":"henri"},"âge":null},{"nom":"michu","prénom":"mariette","âge":58}]`;
await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom", "prénom", "âge"]);
expect(parser.parseResults.datas).toEqual([{ nom:"michu","prénom":"mariette","âge":"58"}]);
});
it("Si toutes les données fournies sont ok, on doit les retrouver en résultat.", async () =>
{
parser.datas2Parse=`[{"nom":"dugenoux","prénom":"henri","âge":25},{"nom":"michu","prénom":"mariette","âge":58}]`;
await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]);
expect(parser.parseResults.datas).toEqual([{nom:"dugenoux","prénom":"henri", "âge":"25"}, {nom:"michu","prénom":"mariette", "âge":"58"}]);
// Tous les objets n'ont pas forcément les mêmes attributs, ni dans le même ordre
parser.datas2Parse=`[{"nom":"dugenoux","prénom":"henri","âge":"25"},{"âge":"58","nom":"michu","pseudo":"madame Michu"}]`;
await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge", "pseudo"]);
expect(parser.parseResults.datas).toEqual([{nom:"dugenoux","prénom":"henri", "âge":"25"}, {"âge":"58", nom:"michu", pseudo:"madame Michu" }]);
});
});
it("Doit générer une erreur si les champs n'ont pas été trouvés dans les données.", async () =>
{
parser.datas2Parse=`{ "field": [" nom","prénom ", " âge "], "datas": [["dugenoux","henri","25"]] }`; // manque un "s" à fields :)
await expectAsync(parser.parse()).toBeRejectedWith(new Error(errors.parserFail));
parser.datas2Parse=`[{" ":"dugenoux"," ":"henri"},{" ":"michu"," ":" "}]`;
await expectAsync(parser.parse()).toBeRejectedWith(new Error(errors.parserFail));
});
});

View File

@ -5,7 +5,8 @@ module.exports =
mode: "development",
entry:
{
firstExample: "./src/firstExample.ts",
exampleWithCSV: "./src/exampleWithCSV.ts",
exampleWithJSON: "./src/exampleWithJSON.ts",
},
output:
{