Ajout classe parseur JSON + script de tests.
This commit is contained in:
parent
d7d9885b2b
commit
7eadd29be2
@ -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": {
|
||||
|
@ -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.",
|
||||
|
@ -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 !== "")
|
||||
|
@ -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;
|
||||
datas2Parse: string;
|
||||
parseResults: ParseResults|undefined;
|
||||
parse(): Promise<void>;
|
||||
}
|
||||
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
|
||||
|
154
src/freeDatas2HTMLParserForJSON.ts
Normal file
154
src/freeDatas2HTMLParserForJSON.ts
Normal 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
190
tests/parserForJSONSpec.ts
Normal 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));
|
||||
});
|
||||
|
||||
});
|
@ -5,7 +5,8 @@ module.exports =
|
||||
mode: "development",
|
||||
entry:
|
||||
{
|
||||
firstExample: "./src/firstExample.ts",
|
||||
exampleWithCSV: "./src/exampleWithCSV.ts",
|
||||
exampleWithJSON: "./src/exampleWithJSON.ts",
|
||||
},
|
||||
output:
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user