diff --git a/package.json b/package.json index 6cd1453..d6bb2fc 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/errors.js b/src/errors.js index ac6b787..fd26fba 100644 --- a/src/errors.js +++ b/src/errors.js @@ -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.", diff --git a/src/freeDatas2HTML.ts b/src/freeDatas2HTML.ts index 3033196..3a2558f 100644 --- a/src/freeDatas2HTML.ts +++ b/src/freeDatas2HTML.ts @@ -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 !== "") diff --git a/src/freeDatas2HTMLInterfaces.ts b/src/freeDatas2HTMLInterfaces.ts index f43619b..57101ff 100644 --- a/src/freeDatas2HTMLInterfaces.ts +++ b/src/freeDatas2HTMLInterfaces.ts @@ -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,21 +59,21 @@ export interface ParseErrors // erreurs non bloquantes rencontrées lors du pars } export interface ParseResults { - datas: []; + datas: {[index: string]:string}[]; errors: ParseErrors[]; fields: string[]; } export interface Parsers { datasRemoteSource: RemoteSource; - datas2Parse:string; - parseResults:ParseResults|undefined; - parse(): Promise; + datas2Parse: string; + parseResults: ParseResults|undefined; + parse(): Promise; } 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 diff --git a/src/freeDatas2HTMLParserForJSON.ts b/src/freeDatas2HTMLParserForJSON.ts new file mode 100644 index 0000000..17086ce --- /dev/null +++ b/src/freeDatas2HTMLParserForJSON.ts @@ -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 + { + 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); + } + } +} \ No newline at end of file diff --git a/tests/parserForJSONSpec.ts b/tests/parserForJSONSpec.ts new file mode 100644 index 0000000..1b8405b --- /dev/null +++ b/tests/parserForJSONSpec.ts @@ -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)); + }); + +}); \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index dc3cd3c..97c6d34 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,7 +5,8 @@ module.exports = mode: "development", entry: { - firstExample: "./src/firstExample.ts", + exampleWithCSV: "./src/exampleWithCSV.ts", + exampleWithJSON: "./src/exampleWithJSON.ts", }, output: {