From e1cc0483eb464857c09eca9687cec1b173ad08d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabrice=20PENHO=C3=8BT?= Date: Mon, 18 Oct 2021 17:22:20 +0200 Subject: [PATCH] Relecture parseur JSON + ajout de tests. --- src/ParserForJSON.ts | 154 +++++++++++++++++++------------------ src/errors.js | 2 + tests/parserForJSONSpec.ts | 112 +++++++++++++++++---------- 3 files changed, 154 insertions(+), 114 deletions(-) diff --git a/src/ParserForJSON.ts b/src/ParserForJSON.ts index 8d00284..dbd8671 100644 --- a/src/ParserForJSON.ts +++ b/src/ParserForJSON.ts @@ -1,7 +1,6 @@ -const errors = require("./errors.js"); +const errors=require("./errors.js"); 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 { @@ -17,7 +16,7 @@ export class ParserForJSON implements Parsers else this._datasRemoteSource=new RemoteSource({ url:"" }); } - + public setRemoteSource(source: RemoteSourceSettings) { this._datasRemoteSource=new RemoteSource(source); @@ -46,20 +45,6 @@ export class ParserForJSON implements Parsers 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 { const parser=this; @@ -77,70 +62,93 @@ export class ParserForJSON implements Parsers 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(Array.isArray(datasParsed.fields) && Array.isArray(datasParsed.datas)) { - 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)) + const nbFields=datasParsed.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++) { - fields=ParserForJSON.trimAllFields(datasParsed.fields); - const nbFields=fields.length, nbDatas=datasParsed.datas.length; - for(let i=0; i < nbDatas; i++) + if(typeof fields[i] !== "string") + parseErrors.push({ row:-1, message: errors.parserTypeError+typeof fields[i] }); + else { - 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); + 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}); } } - 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. + fields=goodFields; + console.log(datasParsed.fields); + console.log( goodFields); + if(fields.length === 0) throw new Error(errors.parserFail); - // datas et errors peuvent par contre rester vides. - parser._parseResults = + // Puis les données : + for(let i=0; i < nbDatas; i++) { - datas: datas, - errors: parseErrors, - fields: fields, - }; + 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++) + { + 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 + parseErrors.push({ row:i, message: errors.parserLineWithoutDatas}); + } } - catch(e) + else // Ou un tableau d'objets {}[], dont les attributs sont les noms des champs { - console.error(e); - throw new Error(errors.parserFail); + 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) + { + field=field.trim(); + 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); + if(dataObject[field] !== undefined) // = doublon + parseErrors.push({ row:i, message:errors.parserFieldNameFail}); + else + dataObject[field]=data[field]+""; // force le type String + } + } + if(Object.keys(dataObject).length !== 0) + datas.push(dataObject); + else + parseErrors.push({ row:i, message: errors.parserLineWithoutDatas}); + i++; + } + if(fields.length === 0) + throw new Error(errors.parserFail); } + parser._parseResults = + { + datas: datas, + errors: parseErrors, + fields: fields, + }; } } \ No newline at end of file diff --git a/src/errors.js b/src/errors.js index 187a120..b2077f9 100644 --- a/src/errors.js +++ b/src/errors.js @@ -14,9 +14,11 @@ module.exports = parserFail: "La lecture des données a échoué.", 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.", + 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.", 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.", + 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.", 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 : ", diff --git a/tests/parserForJSONSpec.ts b/tests/parserForJSONSpec.ts index 981c82c..f32ddf1 100644 --- a/tests/parserForJSONSpec.ts +++ b/tests/parserForJSONSpec.ts @@ -10,9 +10,10 @@ describe("Tests du parseur de JSON", () => 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.datasRemoteSource.url).toEqual(""); }); it("Doit générer une erreur si la chaîne de données à parser est vide.", () => @@ -40,7 +41,7 @@ describe("Tests du parseur de JSON", () => }); describe("Accès à des données distantes.", () => - { + { it("Doit générer une erreur, si l'accès aux données distantes est défaillant.", async () => { parser.setRemoteSource({ url:"http://localhost:9876/datas/posts.jso" }); // une seule lettre vous manque... @@ -53,69 +54,90 @@ describe("Tests du parseur de JSON", () => await parser.parse(); 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.", () => { - 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"]]; - 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"]] }`; + parser.datas2Parse=JSON.stringify({ fields: [" nom", "prénom ", " âge "], datas: [] }); await parser.parse(); 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.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 () => { - 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(); - 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 () => + 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(); expect(parser.parseResults.fields).toEqual(["nom","prénom", "âge"]); 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(); 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.errors.length).toEqual(0); + }); }); - + 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}]`; + parser.datas2Parse=JSON.stringify([{"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"]}]`; + 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(); 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"}]); @@ -123,34 +145,42 @@ describe("Tests du parseur de JSON", () => 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(); + expect(parser.parseResults.datas).toEqual([{nom:"dugenoux", "prénom":"henri","âge":"25"}, {"âge":"58", nom:"michu", "prénom":"mariette"}]); + expect(parser.parseResults.errors[0]).toEqual({row:0,message:errors.parserFieldNameFail }); + expect(parser.parseResults.errors[1]).toEqual({row:1,message:errors.parserFieldNameFail }); + }); + + it("Un enregistrement n'ayant aucune donnée valide doit être ignoré et cela doit être reporté.", async () => + { + parser.datas2Parse=JSON.stringify([{nom:["dugenoux"]},{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"}]); + expect(parser.parseResults.errors[1]).toEqual({row:0,message:errors.parserLineWithoutDatas }); // errors[0] signale l'erreur de type }); - it("Si toutes les données fournies sont ok, on doit les retrouver en résultat.", async () => + it("Doit générer une erreur si aucun nom de champ n'est trouvé dans les données.", async () => { - parser.datas2Parse=`[{"nom":"dugenoux","prénom":"henri","âge":25},{"nom":"michu","prénom":"mariette","âge":58}]`; + 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(); 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.errors.length).toEqual(0); // 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(); 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.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)); - }); - }); \ No newline at end of file