Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement xml and json formatters #178

Merged
merged 5 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,10 @@ export default [
{ languageOptions: { globals: globals.node } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{ ignores: ["dist/"] }
{
rules: {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
},
ignores: ["dist/"]
}
];
5 changes: 3 additions & 2 deletions lib/commands/unipept/unipept_subcommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,12 @@ export abstract class UnipeptSubcommand {
const result = await r.json();

if (this.firstBatch && this.options.header) {
this.firstBatch = false;
this.outputStream.write(this.formatter.header(result, this.fasta));
}

this.outputStream.write(this.formatter.format(result, this.fasta));
this.outputStream.write(this.formatter.format(result, this.fasta, this.firstBatch));

if (this.firstBatch) this.firstBatch = false;
}

private constructRequestBody(slice: string[]): URLSearchParams {
Expand Down
8 changes: 7 additions & 1 deletion lib/formatters/formatter_factory.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { CSVFormatter } from "./csv_formatter.js";
import { Formatter } from "./formatter.js";
import { CSVFormatter } from "./csv_formatter.js";
import { JSONFormatter } from "./json_formatter.js";
import { XMLFormatter } from "./xml_formatter.js";

export class FormatterFactory {
static getFormatter(name: string): Formatter {
if (name === "csv") {
return new CSVFormatter();
} else if (name === "json") {
return new JSONFormatter();
} else if (name === "xml") {
return new XMLFormatter();
}
return new CSVFormatter();
}
Expand Down
17 changes: 17 additions & 0 deletions lib/formatters/json_formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Formatter } from "./formatter.js";

export class JSONFormatter extends Formatter {

header(_sampleData: { [key: string]: string }[], _fastaMapper?: boolean | undefined): string {
return "[";
}

footer(): string {
return "]\n";
}

convert(data: object[], first: boolean): string {
const output = data.map(d => JSON.stringify(d)).join(",");
return first ? output : `,${output}`;
}
}
255 changes: 255 additions & 0 deletions lib/formatters/to_xml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck

// This file was taken from https://github.com/kawanet/to-xml and modified to have a specific output for arrays.

/**
* The toXML() method converts a JavaScript value to an XML string.
*
* @function toXML
* @param value {Object} The value to convert to an XML string.
* @param [replacer] {Function} A function that alters the behavior
* of the stringification process.
* @param [space] {Number|String} A String or Number object that's
* used to insert white space into the output XML string for
* readability purposes. If this is a Number, it indicates the number
* of space characters to use as white space.
* If this is a String, the string is used as white space.
* @returns {String}
*/

const TYPES = {
"boolean": fromString,
"number": fromString,
"object": fromObject,
"string": fromString
};

const ESCAPE = {
"\t": "	",
"\n": "
",
"\r": "
",
" ": " ",
"&": "&",
"<": "&lt;",
">": "&gt;",
'"': "&quot;"
};

const ATTRIBUTE_KEY = "@";
const CHILD_NODE_KEY = "#";
const LF = "\n";

const isArray = Array.isArray || _isArray;

const REPLACE = String.prototype.replace;

function _toXML(value, replacer, space) {
const job = createJob(replacer, space);
fromAny(job, "", value);
return job.r;
}

function createJob(replacer, space) {
const job = {
f: replacer, // replacer function
// s: "", // indent string
// i: 0, // indent string length
l: "", // current indent string
r: "" // result string
};

if (space) {
let str = "";

if (space > 0) {
for (let i = space; i; i--) {
str += " ";
}
} else {
str += space; // stringify
}
job.s = str;

// indent string length
job.i = str.length;
}

return job;
}

function fromAny(job, key, value) {
// child node synonym
if (key === CHILD_NODE_KEY) key = "";

if (_isArray(value)) return fromArray(job, key, value);

const replacer = job.f;
if (replacer) value = replacer(key, value);

const f = TYPES[typeof value];
if (f) f(job, key, value);
}

function fromString(job, key, value) {
if (key === "?") {
// XML declaration
value = "<?" + value + "?>";
} else if (key === "!") {
// comment, CDATA section
value = "<!" + value + ">";
} else {
value = escapeTextNode(value);
if (key) {
// text element without attributes
value = "<" + key + ">" + value + "</" + key + ">";
}
}

if (key && job.i && job.r) {
job.r += LF + job.l; // indent
}

job.r += value;
}

function fromArray(job, key, value) {
if (key !== "item") {
fromObject(job, key, { item: value });
} else {
Array.prototype.forEach.call(value, function (value) {
fromAny(job, key, value);
});
}
}

function fromObject(job, key, value) {
// empty tag
const hasTag = !!key;
const closeTag = (value === null);
if (closeTag) {
if (!hasTag) return;
value = {};
}

const keys = Object.keys(value);
const keyLength = keys.length;
const attrs = keys.filter(isAttribute);
const attrLength = attrs.length;
const hasIndent = job.i;
const curIndent = job.l;
let willIndent = hasTag && hasIndent;
let didIndent;

// open tag
if (hasTag) {
if (hasIndent && job.r) {
job.r += LF + curIndent;
}

job.r += '<' + key;

// attributes
attrs.forEach(function (name) {
writeAttributes(job, name.substr(1), value[name]);
});

// empty element
const isEmpty = closeTag || (attrLength && keyLength === attrLength);
if (isEmpty) {
const firstChar = key[0];
if (firstChar !== "!" && firstChar !== "?") {
job.r += "/";
}
}

job.r += '>';

if (isEmpty) return;
}

keys.forEach(function (name) {
// skip attribute
if (isAttribute(name)) return;

// indent when it has child node but not fragment
if (willIndent && ((name && name !== CHILD_NODE_KEY) || isArray(value[name]))) {
job.l += job.s; // increase indent level
willIndent = 0;
didIndent = 1;
}

// child node or text node
fromAny(job, name, value[name]);
});

if (didIndent) {
// decrease indent level
job.l = job.l.substr(job.i);

job.r += LF + job.l;
}

// close tag
if (hasTag) {
job.r += '</' + key + '>';
}
}

function writeAttributes(job, key, val) {
if (isArray(val)) {
val.forEach(function (child) {
writeAttributes(job, key, child);
});
} else if (!key && "object" === typeof val) {
Object.keys(val).forEach(function (name) {
writeAttributes(job, name, val[name]);
});
} else {
writeAttribute(job, key, val);
}
}

function writeAttribute(job, key, val) {
const replacer = job.f;
if (replacer) val = replacer(ATTRIBUTE_KEY + key, val);
if ("undefined" === typeof val) return;

// empty attribute name
if (!key) {
job.r += ' ' + val;
return;
}

// attribute name
job.r += ' ' + key;

// property attribute
if (val === null) return;

job.r += '="' + escapeAttribute(val) + '"';
}

function isAttribute(name) {
return name && name[0] === ATTRIBUTE_KEY;
}

function escapeTextNode(str) {
return REPLACE.call(str, /(^\s|[&<>]|\s$)/g, escapeRef);
}

function escapeAttribute(str) {
return REPLACE.call(str, /([&"])/g, escapeRef);
}

function escapeRef(str) {
return ESCAPE[str] || str;
}

function _isArray(array) {
return array instanceof Array;
}

export function toXML(value: object, replacer?: function, space?: number | string): string {
return _toXML(value, replacer, space);
}
17 changes: 17 additions & 0 deletions lib/formatters/xml_formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Formatter } from "./formatter.js";
import { toXML } from "./to_xml.js";

export class XMLFormatter extends Formatter {

header(_sampleData: { [key: string]: string }[], _fastaMapper?: boolean | undefined): string {
return "<results>";
}

footer(): string {
return "</results>\n";
}

convert(data: object[], _first: boolean): string {
return data.map(d => `<result>${toXML(d)}</result>`).join("");
}
}
28 changes: 28 additions & 0 deletions tests/formatters/json_formatter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FormatterFactory } from "../../lib/formatters/formatter_factory";
import { TestObject } from "./test_object";

const formatter = FormatterFactory.getFormatter("json");

test('test header', () => {
const object = [TestObject.testObject(), TestObject.testObject()];
expect(formatter.header(object)).toBe("[");
});

test('test footer', () => {
expect(formatter.footer()).toBe("]\n");
});

test('test convert', () => {
const object = [TestObject.testObject()];
const json = TestObject.asJson();

expect(formatter.convert(object, true)).toBe(json);
expect(formatter.convert(object, false)).toBe(`,${json}`);
});

test('test format with fasta', () => {
//const fasta = [['>test', '5']];
//const object = [TestObject.testObject()];
//const json = '{"fasta_header":">test","integer":5,"string":"string","list":["a",2,false]}';
//expect(formatter.format(object, fasta, true)).toBe(json);
});
28 changes: 28 additions & 0 deletions tests/formatters/xml_formatter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FormatterFactory } from "../../lib/formatters/formatter_factory";
import { TestObject } from "./test_object";

const formatter = FormatterFactory.getFormatter("xml");

test('test header', () => {
const object = [TestObject.testObject(), TestObject.testObject()];
expect(formatter.header(object)).toBe("<results>");
});

test('test footer', () => {
expect(formatter.footer()).toBe("</results>\n");
});

test('test convert', () => {
const object = [TestObject.testObject()];
const xml = `<result>${TestObject.asXml()}</result>`;

expect(formatter.convert(object, true)).toBe(xml);
expect(formatter.convert(object, false)).toBe(xml);
});

test('test format with fasta', () => {
//const fasta = [['>test', '5']];
//const object = [TestObject.testObject()];
//const json = '{"fasta_header":">test","integer":5,"string":"string","list":["a",2,false]}';
//expect(formatter.format(object, fasta, true)).toBe(json);
});