Relecture parseur JSON + ajout de tests.

This commit is contained in:
Fabrice PENHOËT 2021-10-18 17:22:20 +02:00
parent c61b9ef402
commit e1cc0483eb
3 changed files with 154 additions and 114 deletions

View File

@ -1,6 +1,5 @@
const errors=require("./errors.js"); const errors=require("./errors.js");
import { RemoteSource } from "./RemoteSource"; import { RemoteSource } from "./RemoteSource";
import { ParseErrors, ParseResults, Parsers, RemoteSources, RemoteSourceSettings } from "./interfaces"; import { ParseErrors, ParseResults, Parsers, RemoteSources, RemoteSourceSettings } from "./interfaces";
export class ParserForJSON implements Parsers export class ParserForJSON implements Parsers
@ -46,20 +45,6 @@ export class ParserForJSON implements Parsers
return this._parseResults; 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> public async parse(): Promise<any>
{ {
const parser=this; const parser=this;
@ -77,19 +62,40 @@ export class ParserForJSON implements Parsers
else else
throw new Error(errors.parserNeedSource); throw new Error(errors.parserNeedSource);
try
{
const datasParsed=JSON.parse(parseContent); const datasParsed=JSON.parse(parseContent);
const typesOkForValue=["boolean","number","string"]; const typesOkForValue=["boolean","number","string"];
let fields: string[]=[], datas: {[index: string]:string}[]=[], parseErrors: ParseErrors[]=[]; 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[][] // 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)) if(Array.isArray(datasParsed.fields) && Array.isArray(datasParsed.datas))
{ {
fields=ParserForJSON.trimAllFields(datasParsed.fields); const nbFields=datasParsed.fields.length, nbDatas=datasParsed.datas.length;
const nbFields=fields.length, nbDatas=datasParsed.datas.length; // Traitement des noms de champs reçus :
const goodFields: string[]=[];
fields=datasParsed.fields;
for(let i=0; i < nbFields; i++)
{
if(typeof fields[i] !== "string")
parseErrors.push({ row:-1, message: errors.parserTypeError+typeof fields[i] });
else
{
fields[i]=fields[i].trim();
if(fields[i] !== "" && goodFields.indexOf(fields[i]) === -1)
goodFields.push(fields[i]);
else
parseErrors.push({ row:-1, message: errors.parserFieldNameFail});
}
}
fields=goodFields;
console.log(datasParsed.fields);
console.log( goodFields);
if(fields.length === 0)
throw new Error(errors.parserFail);
// Puis les données :
for(let i=0; i < nbDatas; i++) for(let i=0; i < nbDatas; i++)
{ {
const dataObject: {[index: string]: string}={}, nbObjFields=datasParsed.datas[i].length; const dataObject: {[index: string]: string}={}, nbObjFields=datasParsed.datas[i].length;
if( nbObjFields !== nbFields)
parseErrors.push({ row:i, message:errors.parserNumberOfFieldsFail});
for(let j=0; j < nbObjFields && j < nbFields; j++) for(let j=0; j < nbObjFields && j < nbFields; j++)
{ {
if(typesOkForValue.indexOf(typeof datasParsed.datas[i][j]) === -1) if(typesOkForValue.indexOf(typeof datasParsed.datas[i][j]) === -1)
@ -99,6 +105,8 @@ export class ParserForJSON implements Parsers
} }
if(Object.keys(dataObject).length !== 0) if(Object.keys(dataObject).length !== 0)
datas.push(dataObject); datas.push(dataObject);
else
parseErrors.push({ row:i, message: errors.parserLineWithoutDatas});
} }
} }
else // Ou un tableau d'objets {}[], dont les attributs sont les noms des champs else // Ou un tableau d'objets {}[], dont les attributs sont les noms des champs
@ -107,29 +115,35 @@ export class ParserForJSON implements Parsers
for(let data of datasParsed) for(let data of datasParsed)
{ {
// Ici les champs sont découverts au fur et à mesure, // Ici les champs sont découverts au fur et à mesure,
// leur ordre peut être différent d'une ligne à l'autre // Leur ordre peut être différent d'une ligne à l'autre
// et tous les champs ne sont pas systématiquement présents // Et tous les champs ne sont pas systématiquement présents
let dataObject: {[index: string]: string} = {} let dataObject: {[index: string]: string} = {}
for(let field in data) for(let field in data)
{
if(typesOkForValue.indexOf(typeof data[field]) !== -1)
{ {
field=field.trim(); field=field.trim();
if(field !== "" && fields.indexOf(field) === -1) if(field === "")
parseErrors.push({ row:-1, message: errors.parserFieldNameFail});
else if (typesOkForValue.indexOf(typeof data[field]) === -1)
parseErrors.push({ row:i, message:errors.parserTypeError+typeof data[field]});
else
{
if(fields.indexOf(field) === -1)
fields.push(field); fields.push(field);
if(dataObject[field] !== undefined) // = doublon
parseErrors.push({ row:i, message:errors.parserFieldNameFail});
else
dataObject[field]=data[field]+""; // force le type String 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) if(Object.keys(dataObject).length !== 0)
datas.push(dataObject); datas.push(dataObject);
else
parseErrors.push({ row:i, message: errors.parserLineWithoutDatas});
i++; i++;
} }
} if(fields.length === 0)
if(fields.length === 0) // possible si données fournies non correctement formées.
throw new Error(errors.parserFail); throw new Error(errors.parserFail);
// datas et errors peuvent par contre rester vides. }
parser._parseResults = parser._parseResults =
{ {
datas: datas, datas: datas,
@ -137,10 +151,4 @@ export class ParserForJSON implements Parsers
fields: fields, fields: fields,
}; };
} }
catch(e)
{
console.error(e);
throw new Error(errors.parserFail);
}
}
} }

View File

@ -14,9 +14,11 @@ module.exports =
parserFail: "La lecture des données a échoué.", parserFail: "La lecture des données a échoué.",
parserFieldNameFail: "Les noms de champs fournis doivent être uniques et ne peuvent être vides.", parserFieldNameFail: "Les noms de champs fournis doivent être uniques et ne peuvent être vides.",
parserFieldsNotFound: "Aucun nom de champs n'a été trouvé par le parseur.", parserFieldsNotFound: "Aucun nom de champs n'a été trouvé par le parseur.",
parserLineWithoutDatas: "Une ligne ne contenant aucune donnée valide a été trouvée.",
parserMeetErrors : "Au moins une erreur a été rencontrée durant le traitement des données.", parserMeetErrors : "Au moins une erreur a été rencontrée durant le traitement des données.",
parserNeedDatas: "Merci de fournir une chaîne de caractères valide à parser.", 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.", parserNeedSource: "Merci de fournir une chaîne de caractères où une url pour les données à parser.",
parserNumberOfFieldsFail: "Il n'y a pas le nombre de champs attendu pour cet enregistrement.",
parserRemoteFail: "Erreur rencontrée durant l'accès aux données distantes.", parserRemoteFail: "Erreur rencontrée durant l'accès aux données distantes.",
parserSelectorsIsEmpty: "Les sélecteurs CSS ne peuvent pas être une chaîne vide.", parserSelectorsIsEmpty: "Les sélecteurs CSS ne peuvent pas être une chaîne vide.",
parserTypeError: "Une donnée a été trouvée avec un type imprévu : ", parserTypeError: "Une donnée a été trouvée avec un type imprévu : ",

View File

@ -10,9 +10,10 @@ describe("Tests du parseur de JSON", () =>
parser=new Parser(); parser=new Parser();
}); });
it("Doit avoir créé une instance du Parser", () => it("Doit avoir créé une instance du Parser et initialiser la ressource distante avec une url vide.", () =>
{ {
expect(parser).toBeInstanceOf(Parser); expect(parser).toBeInstanceOf(Parser);
expect(parser.datasRemoteSource.url).toEqual("");
}); });
it("Doit générer une erreur si la chaîne de données à parser est vide.", () => it("Doit générer une erreur si la chaîne de données à parser est vide.", () =>
@ -53,54 +54,75 @@ describe("Tests du parseur de JSON", () =>
await parser.parse(); await parser.parse();
expect(parser.parseResults).not.toBeUndefined(); expect(parser.parseResults).not.toBeUndefined();
}); });
it("Si des options de connexion distante sont fournies, elles doivent être utilisées.", async () =>
{
spyOn(window, "fetch").and.callThrough();
parser.setRemoteSource({ url:"http://localhost:9876/datas/posts.json", headers: [{ key:"token", value:"1234" }, { key:"userName", value:"Toto" }], withCredentials:true });
await parser.parse();
const headers=new Headers();
headers.append("token", "1234");
headers.append("userName", "Toto");
expect(window.fetch).toHaveBeenCalledWith("http://localhost:9876/datas/posts.json", { method: "GET", headers: headers, credentials: "include" });
});
}); });
describe("Noms des champs et données fournies dans deux tableaux distincts.", () => 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.", () => it("Les espaces entourant les noms de champs doivent être supprimés.", async () =>
{ {
const fields=["nom",24,"prénom", true,{ field:"champ"},"âge",["je suis un nom de champ"]]; parser.datas2Parse=JSON.stringify({ fields: [" nom", "prénom ", " âge "], datas: [] });
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(); await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]); expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]);
});
it("Les noms de champs vides, en doublon ou autre qu'une chaîne de caractère doivent ête refusés et l'erreur reportée.", async () =>
{
parser.datas2Parse=JSON.stringify({ fields: ["nom", 24, "prénom", true,{ field:"champ"}, "âge", ["je suis un nom de champ"], " ", "nom"], datas: [] });
await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]);
expect(parser.parseResults.errors).toEqual([{ row:-1, message: errors.parserTypeError+"number" },{ row:-1, message: errors.parserTypeError+"boolean" }, { row:-1, message: errors.parserTypeError+"object" }, { row:-1, message: errors.parserTypeError+"object" },{ row:-1, message: errors.parserFieldNameFail}, { row:-1, message: errors.parserFieldNameFail}]);
});
it("Une erreur doit être générée si aucun nom de champ valide n'est trouvé.", async () =>
{
parser.datas2Parse=JSON.stringify({ fields: [{ field:"champ"}, " "], datas: [] });
await expectAsync(parser.parse()).toBeRejectedWith(new Error(errors.parserFail));
});
it("Si une ligne contient un nombre de champs différents que celui attendu, l'erreur doit être reportée. Les champs en trop sont ignorés.", async () =>
{
parser.datas2Parse=JSON.stringify({ fields: ["nom","prénom", "âge"], datas: [["dugenoux","henri",25,"je me champ en trop"],["michu","mariette"]] });
await parser.parse();
expect(parser.parseResults.datas).toEqual([{nom:"dugenoux","prénom":"henri", "âge":"25"}, {nom:"michu","prénom":"mariette"}]); expect(parser.parseResults.datas).toEqual([{nom:"dugenoux","prénom":"henri", "âge":"25"}, {nom:"michu","prénom":"mariette"}]);
expect(parser.parseResults.errors).toEqual([{ row:0, message:errors.parserNumberOfFieldsFail}, { row:1, message:errors.parserNumberOfFieldsFail}]);
}); });
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 () => 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]] }`; parser.datas2Parse=JSON.stringify({ fields: ["nom","prénom", "âge"], datas: [["dugenoux",{ "prenom":"henri"},25],["michu","mariette",null]] });
await parser.parse(); 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.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[0]).toEqual({row:0,message:errors.parserTypeError+"object"});
expect(parser.parseResults.errors[1]).toEqual({row:1,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 () => it("Un enregistrement n'ayant aucune donnée valide sera ignoré. L'erreur est reportée.", async () =>
{ {
parser.datas2Parse=`{ "fields": ["nom","prénom", "âge"], "datas": [["dugenoux","henri",25],[null,{ "prenom":"mariette"},[58]]] }`; parser.datas2Parse=JSON.stringify({ fields: ["nom","prénom", "âge"], datas: [["dugenoux","henri",25],[null,{ "prenom":"mariette"},[58]]] });
await parser.parse(); await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]); expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]);
expect(parser.parseResults.datas).toEqual([{nom:"dugenoux","prénom":"henri", "âge":"25"}]); expect(parser.parseResults.datas).toEqual([{nom:"dugenoux","prénom":"henri", "âge":"25"}]);
expect(parser.parseResults.errors[3]).toEqual({row:1,message:errors.parserLineWithoutDatas});
}); });
it("Si toutes les données fournies sont ok, on doit les retrouver en résultat.", async () => it("Si toutes les données fournies sont ok, on doit les retrouver en résultat et aucune erreur n'est reportée.", async () =>
{ {
parser.datas2Parse=`{ "fields": ["nom","prénom", "âge"], "datas": [["dugenoux","henri",25],["michu","mariette",58]] }`; parser.datas2Parse=JSON.stringify({ fields: ["nom","prénom", "âge"], datas: [["dugenoux","henri",25],["michu","mariette",58]] });
await parser.parse(); await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]); 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"}]); expect(parser.parseResults.datas).toEqual([{nom:"dugenoux","prénom":"henri", "âge":"25"}, {nom:"michu","prénom":"mariette", "âge":"58"}]);
expect(parser.parseResults.errors.length).toEqual(0);
}); });
}); });
@ -108,14 +130,14 @@ describe("Tests du parseur de JSON", () =>
{ {
it("Les espaces entourant les noms de champs doivent être supprimés.", async () => 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}]`; parser.datas2Parse=JSON.stringify([{"nom ":"dugenoux"," prénom":"henri"," âge ":25},{nom:"michu","prénom":"mariette","âge":58}]);
await parser.parse(); await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]); 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 () => 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"]}]`; parser.datas2Parse=JSON.stringify([{nom:"dugenoux","prénom":{"value":"henri"},"âge":25},{"âge":"58",nom:"michu","prénom":"mariette",pseudo:["madame Michu"]}]);
await parser.parse(); await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom", "âge", "prénom"]); 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.datas).toEqual([{nom:"dugenoux", "âge":"25"}, {"âge":"58", nom:"michu", "prénom":"mariette"}]);
@ -123,34 +145,42 @@ describe("Tests du parseur de JSON", () =>
expect(parser.parseResults.errors[1]).toEqual({row:1,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 () => it("Si certaines des données fournies déclare plusieurs fois le même attribut, elles doivent être ignorées. Et les erreurs doivent être reportées.", async () =>
{ {
parser.datas2Parse=`[{"nom":["dugenoux"],"prénom":{"value":"henri"},"âge":null},{"nom":"michu","prénom":"mariette","âge":58}]`; parser.datas2Parse=JSON.stringify([{nom:"dugenoux","prénom":"henri","âge":25, "prénom ":"Henry"},{"âge":"58",nom:"michu","prénom":"mariette", " âge ":"48" }]);
await parser.parse(); await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom", "prénom", "âge"]); expect(parser.parseResults.datas).toEqual([{nom:"dugenoux", "prénom":"henri","âge":"25"}, {"âge":"58", nom:"michu", "prénom":"mariette"}]);
expect(parser.parseResults.datas).toEqual([{ nom:"michu","prénom":"mariette","âge":"58"}]); expect(parser.parseResults.errors[0]).toEqual({row:0,message:errors.parserFieldNameFail });
expect(parser.parseResults.errors[1]).toEqual({row:1,message:errors.parserFieldNameFail });
}); });
it("Si toutes les données fournies sont ok, on doit les retrouver en résultat.", async () => it("Un enregistrement n'ayant aucune donnée valide doit être ignoré et cela doit être reporté.", async () =>
{ {
parser.datas2Parse=`[{"nom":"dugenoux","prénom":"henri","âge":25},{"nom":"michu","prénom":"mariette","âge":58}]`; parser.datas2Parse=JSON.stringify([{nom:["dugenoux"]},{nom:"michu","prénom":"mariette","âge":58}]);
await parser.parse();
expect(parser.parseResults.datas).toEqual([{ nom:"michu","prénom":"mariette","âge":"58"}]);
expect(parser.parseResults.errors[1]).toEqual({row:0,message:errors.parserLineWithoutDatas }); // errors[0] signale l'erreur de type
});
it("Doit générer une erreur si aucun nom de champ n'est trouvé dans les données.", async () =>
{
parser.datas2Parse=JSON.stringify([{" ":"dugenoux"," ":"henri"},{" ":"michu"," ":" "}]);
await expectAsync(parser.parse()).toBeRejectedWith(new Error(errors.parserFail));
});
it("Si toutes les données fournies sont ok, on doit les retrouver en résultat et aucune erreur n'est reportée.", async () =>
{
parser.datas2Parse=JSON.stringify([{nom:"dugenoux","prénom":"henri","âge":25},{nom:"michu","prénom":"mariette","âge":58}]);
await parser.parse(); await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]); 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"}]); expect(parser.parseResults.datas).toEqual([{nom:"dugenoux","prénom":"henri", "âge":"25"}, {nom:"michu","prénom":"mariette", "âge":"58"}]);
expect(parser.parseResults.errors.length).toEqual(0);
// Tous les objets n'ont pas forcément les mêmes attributs, ni dans le même ordre // 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"}]`; parser.datas2Parse=JSON.stringify([{nom:"dugenoux","prénom":"henri","âge":"25"},{"âge":"58",nom:"michu",pseudo:"madame Michu"}]);
await parser.parse(); await parser.parse();
expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge", "pseudo"]); 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" }]); expect(parser.parseResults.datas).toEqual([{nom:"dugenoux","prénom":"henri", "âge":"25"}, {"âge":"58", nom:"michu", pseudo:"madame Michu" }]);
expect(parser.parseResults.errors.length).toEqual(0);
}); });
}); });
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));
});
}); });