From 9fef29aa78eca122276662fa8772240d8bd9a5bd Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Tue, 2 Jul 2024 14:45:32 +0200 Subject: [PATCH 01/16] implement basic version of the pept2lca command --- bin/unipept.ts | 6 +++ lib/commands/unipept.ts | 28 ++++++++++++ lib/commands/unipept/pept2lca.ts | 37 +++++++++++++++ lib/commands/unipept/unipept_subcommand.ts | 53 ++++++++++++++++++++++ package.json | 2 + tests/commands/unipept.test.ts | 0 6 files changed, 126 insertions(+) create mode 100755 bin/unipept.ts create mode 100644 lib/commands/unipept.ts create mode 100644 lib/commands/unipept/pept2lca.ts create mode 100644 lib/commands/unipept/unipept_subcommand.ts create mode 100644 tests/commands/unipept.test.ts diff --git a/bin/unipept.ts b/bin/unipept.ts new file mode 100755 index 00000000..bbb5e051 --- /dev/null +++ b/bin/unipept.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node + +import { Unipept } from '../lib/commands/unipept.js'; + +const command = new Unipept(); +command.run(); diff --git a/lib/commands/unipept.ts b/lib/commands/unipept.ts new file mode 100644 index 00000000..553a5fc2 --- /dev/null +++ b/lib/commands/unipept.ts @@ -0,0 +1,28 @@ +import { BaseCommand } from './base_command.js'; +import { Pept2lca } from './unipept/pept2lca.js'; + +export class Unipept extends BaseCommand { + + readonly description = `The unipept subcommands are command line wrappers around the Unipept web services. + +Subcommands that start with pept expect a list of tryptic peptides as input. Subcommands that start with tax expect a list of NCBI Taxonomy Identifiers as input. Input is passed + +- as separate command line arguments +- in a text file that is passed as an argument to the -i option +- to standard input + +The command will give priority to the first way the input is passed, in the order as listed above. Text files and standard input should have one tryptic peptide or one NCBI Taxonomy Identifier per line.`; + + constructor(options?: { exitOverride?: boolean, suppressOutput?: boolean, args?: string[] }) { + super(options); + + this.program + .summary("Command line interface to Unipept web services.") + .description(this.description) + .addCommand(new Pept2lca().command); + } + + async run() { + this.parseArguments(); + } +} diff --git a/lib/commands/unipept/pept2lca.ts b/lib/commands/unipept/pept2lca.ts new file mode 100644 index 00000000..561e73b4 --- /dev/null +++ b/lib/commands/unipept/pept2lca.ts @@ -0,0 +1,37 @@ +import { Option } from "commander"; +import { UnipeptSubcommand } from "./unipept_subcommand.js"; + +export class Pept2lca extends UnipeptSubcommand { + + readonly description = `For each tryptic peptide the unipept pept2lca command retrieves from Unipept the lowest common ancestor of the set of taxa from all UniProt entries whose protein sequence contains an exact matches to the tryptic peptide. The lowest common ancestor is based on the topology of the Unipept Taxonomy -- a cleaned up version of the NCBI Taxonomy -- and is itself a record from the NCBI Taxonomy. The command expects a list of tryptic peptides that are passed + +- as separate command line arguments +- in a text file that is passed as an argument to the -i option +- to standard input + +The command will give priority to the first way tryptic peptides are passed, in the order as listed above. Text files and standard input should have one tryptic peptide per line.`; + + constructor() { + super("pept2lca"); + + this.command + .summary("Fetch taxonomic lowest common ancestor of UniProt entries that match tryptic peptides.") + .description(this.description) + .option("-e, --equate", "equate isoleucine (I) and leucine (L) when matching peptides") + .option("-a, --all", "report all information fields of NCBI Taxonomy records available in Unipept. Note that this may have a performance penalty.") + .addOption(new Option("-s --select ", "select the information fields to return. Selected fields are passed as a comma separated list of field names. Multiple -s (or --select) options may be used.")) + .argument("[peptides...]", "optionally, 1 or more peptides") + .action((args, options) => this.run(args, options)); + } + + async run(args: string[], options: object) { + super.run(args, options); + + for (const peptide of args) { + console.log(peptide); + const r = await fetch(this.url + "?input=" + peptide); + console.log(await r.json()); + } + + } +} diff --git a/lib/commands/unipept/unipept_subcommand.ts b/lib/commands/unipept/unipept_subcommand.ts new file mode 100644 index 00000000..a97a6d19 --- /dev/null +++ b/lib/commands/unipept/unipept_subcommand.ts @@ -0,0 +1,53 @@ +import { Command, Option } from "commander"; +import { readFileSync } from "fs"; + +export abstract class UnipeptSubcommand { + public command: Command; + static readonly VALID_FORMATS = ["blast", "csv", "json", "xml"]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options: any = {}; + name: string; + user_agent: string; + host = "https://api.unipept.ugent.be"; + url: string; + + constructor(name: string) { + this.name = name; + const version = JSON.parse(readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version; + this.user_agent = `unipept-cli/${version}`; + this.command = this.create(name); + } + + create(name: string): Command { + const command = new Command(name); + + command.option("-q, --quiet", "disable service messages"); + command.option("-i, -input ", "read input from file"); + command.option("-o, --output ", "write output to file"); + command.addOption(new Option("-f, --format ", "define the output format").choices(UnipeptSubcommand.VALID_FORMATS).default("json")); + command.option("--host ", "specify the server running the Unipept web service"); + + // internal options + command.addOption(new Option("--no-header", "disable the header in csv output").hideHelp()); + command.addOption(new Option("--batch ", "specify the batch size").hideHelp()); + + return command; + } + + run(args: string[], options: object): void { + this.options = options; + this.host = this.getHost(); + this.url = `${this.host}/api/v2/${this.name}.json`; + } + + getHost(): string { + const host = this.options.host || this.host; + + // add http:// if needed + if (host.startsWith("http://") || host.startsWith("https://")) { + return host; + } else { + return `http://${host}`; + } + } +} diff --git a/package.json b/package.json index f27f3c4c..3e69dd9e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "bin": { "peptfilter": "./bin/peptfilter.js", "prot2pept": "./bin/prot2pept.js", + "unipept": "./bin/unipept.js", "uniprot": "./bin/uniprot.js" }, "scripts": { @@ -19,6 +20,7 @@ "typecheck": "yarn tsc --skipLibCheck --noEmit", "peptfilter": "yarn run tsx bin/peptfilter.ts", "prot2pept": "yarn run tsx bin/prot2pept.ts", + "unipept": "yarn run tsx bin/unipept.ts", "uniprot": "yarn run tsx bin/uniprot.ts" }, "dependencies": { diff --git a/tests/commands/unipept.test.ts b/tests/commands/unipept.test.ts new file mode 100644 index 00000000..e69de29b From bd3d031e8d582123f4a31c5138aae50653cb5f64 Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Tue, 2 Jul 2024 14:52:54 +0200 Subject: [PATCH 02/16] use post and set headers --- lib/commands/unipept/pept2lca.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/commands/unipept/pept2lca.ts b/lib/commands/unipept/pept2lca.ts index 561e73b4..d9ea25db 100644 --- a/lib/commands/unipept/pept2lca.ts +++ b/lib/commands/unipept/pept2lca.ts @@ -28,8 +28,10 @@ The command will give priority to the first way tryptic peptides are passed, in super.run(args, options); for (const peptide of args) { - console.log(peptide); - const r = await fetch(this.url + "?input=" + peptide); + const r = await fetch(this.url + "?input=" + peptide, { + method: "POST", + body: JSON.stringify({ input: [peptide] }) + }); console.log(await r.json()); } From 33353b1f8378d2d68fdd095686215ea2b868ec04 Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Tue, 2 Jul 2024 15:20:38 +0200 Subject: [PATCH 03/16] move processing loop to the super class --- lib/commands/unipept/pept2lca.ts | 11 +---------- lib/commands/unipept/unipept_subcommand.ts | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/commands/unipept/pept2lca.ts b/lib/commands/unipept/pept2lca.ts index d9ea25db..3c56257a 100644 --- a/lib/commands/unipept/pept2lca.ts +++ b/lib/commands/unipept/pept2lca.ts @@ -25,15 +25,6 @@ The command will give priority to the first way tryptic peptides are passed, in } async run(args: string[], options: object) { - super.run(args, options); - - for (const peptide of args) { - const r = await fetch(this.url + "?input=" + peptide, { - method: "POST", - body: JSON.stringify({ input: [peptide] }) - }); - console.log(await r.json()); - } - + await super.run(args, options); } } diff --git a/lib/commands/unipept/unipept_subcommand.ts b/lib/commands/unipept/unipept_subcommand.ts index a97a6d19..d0353431 100644 --- a/lib/commands/unipept/unipept_subcommand.ts +++ b/lib/commands/unipept/unipept_subcommand.ts @@ -9,7 +9,7 @@ export abstract class UnipeptSubcommand { name: string; user_agent: string; host = "https://api.unipept.ugent.be"; - url: string; + url?: string; constructor(name: string) { this.name = name; @@ -34,13 +34,25 @@ export abstract class UnipeptSubcommand { return command; } - run(args: string[], options: object): void { + async run(args: string[], options: object): Promise { this.options = options; this.host = this.getHost(); this.url = `${this.host}/api/v2/${this.name}.json`; + + for (const input of args) { + const r = await fetch(this.url, { + method: "POST", + body: new URLSearchParams({ "input": input }), + headers: { + "Accept-Encoding": "gzip", + "User-Agent": this.user_agent, + } + }); + console.log(await r.json()); + } } - getHost(): string { + private getHost(): string { const host = this.options.host || this.host; // add http:// if needed From c62a0b2ee9c55867b2393678320ed6bb497c2c73 Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Tue, 2 Jul 2024 15:32:39 +0200 Subject: [PATCH 04/16] implement input batches --- lib/commands/unipept/pept2lca.ts | 4 +++ lib/commands/unipept/unipept_subcommand.ts | 33 ++++++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/lib/commands/unipept/pept2lca.ts b/lib/commands/unipept/pept2lca.ts index 3c56257a..5fdc0239 100644 --- a/lib/commands/unipept/pept2lca.ts +++ b/lib/commands/unipept/pept2lca.ts @@ -24,6 +24,10 @@ The command will give priority to the first way tryptic peptides are passed, in .action((args, options) => this.run(args, options)); } + defaultBatchSize(): number { + return 100; + } + async run(args: string[], options: object) { await super.run(args, options); } diff --git a/lib/commands/unipept/unipept_subcommand.ts b/lib/commands/unipept/unipept_subcommand.ts index d0353431..e33490e1 100644 --- a/lib/commands/unipept/unipept_subcommand.ts +++ b/lib/commands/unipept/unipept_subcommand.ts @@ -39,19 +39,34 @@ export abstract class UnipeptSubcommand { this.host = this.getHost(); this.url = `${this.host}/api/v2/${this.name}.json`; + let slice = []; + for (const input of args) { - const r = await fetch(this.url, { - method: "POST", - body: new URLSearchParams({ "input": input }), - headers: { - "Accept-Encoding": "gzip", - "User-Agent": this.user_agent, - } - }); - console.log(await r.json()); + slice.push(input); + if (slice.length >= this.defaultBatchSize()) { + await this.processBatch(slice); + slice = []; + } } + await this.processBatch(slice); + + } + + async processBatch(slice: string[]): Promise { + const r = await fetch(this.url as string, { + method: "POST", + body: new URLSearchParams({ "input": JSON.stringify(slice) }), + headers: { + "Accept-Encoding": "gzip", + "User-Agent": this.user_agent, + } + }); + console.log(await r.json()); } + + abstract defaultBatchSize(): number; + private getHost(): string { const host = this.options.host || this.host; From 1b30a66588492784ae7bd5eff50627dfe6425f27 Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Tue, 2 Jul 2024 15:46:54 +0200 Subject: [PATCH 05/16] use different input options if available --- lib/commands/unipept/unipept_subcommand.ts | 23 +++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/commands/unipept/unipept_subcommand.ts b/lib/commands/unipept/unipept_subcommand.ts index e33490e1..b03c5b40 100644 --- a/lib/commands/unipept/unipept_subcommand.ts +++ b/lib/commands/unipept/unipept_subcommand.ts @@ -1,5 +1,7 @@ import { Command, Option } from "commander"; -import { readFileSync } from "fs"; +import { createReadStream, readFileSync } from "fs"; +import { createInterface } from "node:readline"; +import { Interface } from "readline"; export abstract class UnipeptSubcommand { public command: Command; @@ -41,7 +43,7 @@ export abstract class UnipeptSubcommand { let slice = []; - for (const input of args) { + for await (const input of this.getInputIterator(args, options)) { slice.push(input); if (slice.length >= this.defaultBatchSize()) { await this.processBatch(slice); @@ -49,7 +51,6 @@ export abstract class UnipeptSubcommand { } } await this.processBatch(slice); - } async processBatch(slice: string[]): Promise { @@ -64,6 +65,22 @@ export abstract class UnipeptSubcommand { console.log(await r.json()); } + /** + * Returns an input iterator to use for the request. + * - if arguments are given, use arguments + * - if an input file is given, use the file + * - otherwise, use standard input + */ + getInputIterator(args: string[], options: { input?: string }): string[] | Interface { + if (args.length > 0) { + return args; + } else if (options.input) { + return createInterface({ input: createReadStream(options.input) }); + } else { + return createInterface({ input: process.stdin }) + } + } + abstract defaultBatchSize(): number; From d13561190a6a88e8409f5f125f102ac4948da7b1 Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Tue, 2 Jul 2024 16:38:13 +0200 Subject: [PATCH 06/16] parse more parameters --- lib/commands/unipept/unipept_subcommand.ts | 34 +++++++++++++++------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/commands/unipept/unipept_subcommand.ts b/lib/commands/unipept/unipept_subcommand.ts index b03c5b40..873a24bc 100644 --- a/lib/commands/unipept/unipept_subcommand.ts +++ b/lib/commands/unipept/unipept_subcommand.ts @@ -18,13 +18,15 @@ export abstract class UnipeptSubcommand { const version = JSON.parse(readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version; this.user_agent = `unipept-cli/${version}`; this.command = this.create(name); + this.fasta = false; } + abstract defaultBatchSize(): number; create(name: string): Command { const command = new Command(name); command.option("-q, --quiet", "disable service messages"); - command.option("-i, -input ", "read input from file"); + command.option("-i, --input ", "read input from file"); command.option("-o, --output ", "write output to file"); command.addOption(new Option("-f, --format ", "define the output format").choices(UnipeptSubcommand.VALID_FORMATS).default("json")); command.option("--host ", "specify the server running the Unipept web service"); @@ -36,14 +38,14 @@ export abstract class UnipeptSubcommand { return command; } - async run(args: string[], options: object): Promise { + async run(args: string[], options: { input?: string }): Promise { this.options = options; this.host = this.getHost(); this.url = `${this.host}/api/v2/${this.name}.json`; let slice = []; - for await (const input of this.getInputIterator(args, options)) { + for await (const input of this.getInputIterator(args, options.input)) { slice.push(input); if (slice.length >= this.defaultBatchSize()) { await this.processBatch(slice); @@ -56,7 +58,7 @@ export abstract class UnipeptSubcommand { async processBatch(slice: string[]): Promise { const r = await fetch(this.url as string, { method: "POST", - body: new URLSearchParams({ "input": JSON.stringify(slice) }), + body: this.constructRequestBody(slice), headers: { "Accept-Encoding": "gzip", "User-Agent": this.user_agent, @@ -65,25 +67,37 @@ export abstract class UnipeptSubcommand { console.log(await r.json()); } + private constructRequestBody(slice: string[]): URLSearchParams { + const names = this.constructSelectedFields().length === 0 || this.constructSelectedFields().some(regex => regex.toString().includes("name") || regex.toString().includes(".*$")); + return new URLSearchParams({ + input: JSON.stringify(slice), + equate_il: this.options.equate, + extra: this.options.all, + names: this.options.all && names + }); + } + + // TODO: implement + private constructSelectedFields(): RegExp[] { + return []; + } + /** * Returns an input iterator to use for the request. * - if arguments are given, use arguments * - if an input file is given, use the file * - otherwise, use standard input */ - getInputIterator(args: string[], options: { input?: string }): string[] | Interface { + private getInputIterator(args: string[], input?: string): string[] | Interface { if (args.length > 0) { return args; - } else if (options.input) { - return createInterface({ input: createReadStream(options.input) }); + } else if (input) { + return createInterface({ input: createReadStream(input) }); } else { return createInterface({ input: process.stdin }) } } - - abstract defaultBatchSize(): number; - private getHost(): string { const host = this.options.host || this.host; From 5da999ccd66f37c187c3b9f09dc7a44f9f9ea6aa Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Tue, 2 Jul 2024 17:13:48 +0200 Subject: [PATCH 07/16] add a few utility methods --- lib/commands/unipept/pept2lca.ts | 8 ++--- lib/commands/unipept/unipept_subcommand.ts | 35 ++++++++++++++++++---- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/lib/commands/unipept/pept2lca.ts b/lib/commands/unipept/pept2lca.ts index 5fdc0239..97b8a871 100644 --- a/lib/commands/unipept/pept2lca.ts +++ b/lib/commands/unipept/pept2lca.ts @@ -24,11 +24,11 @@ The command will give priority to the first way tryptic peptides are passed, in .action((args, options) => this.run(args, options)); } - defaultBatchSize(): number { - return 100; + requiredFields(): string[] { + return ["peptide"]; } - async run(args: string[], options: object) { - await super.run(args, options); + defaultBatchSize(): number { + return 100; } } diff --git a/lib/commands/unipept/unipept_subcommand.ts b/lib/commands/unipept/unipept_subcommand.ts index 873a24bc..dca448ce 100644 --- a/lib/commands/unipept/unipept_subcommand.ts +++ b/lib/commands/unipept/unipept_subcommand.ts @@ -12,6 +12,8 @@ export abstract class UnipeptSubcommand { user_agent: string; host = "https://api.unipept.ugent.be"; url?: string; + selectedFields?: RegExp[]; + fasta: boolean; constructor(name: string) { this.name = name; @@ -22,6 +24,10 @@ export abstract class UnipeptSubcommand { } abstract defaultBatchSize(): number; + requiredFields(): string[] { + return []; + } + create(name: string): Command { const command = new Command(name); @@ -47,7 +53,7 @@ export abstract class UnipeptSubcommand { for await (const input of this.getInputIterator(args, options.input)) { slice.push(input); - if (slice.length >= this.defaultBatchSize()) { + if (slice.length >= this.batchSize) { await this.processBatch(slice); slice = []; } @@ -68,7 +74,7 @@ export abstract class UnipeptSubcommand { } private constructRequestBody(slice: string[]): URLSearchParams { - const names = this.constructSelectedFields().length === 0 || this.constructSelectedFields().some(regex => regex.toString().includes("name") || regex.toString().includes(".*$")); + const names = this.getSelectedFields().length === 0 || this.getSelectedFields().some(regex => regex.toString().includes("name") || regex.toString().includes(".*$")); return new URLSearchParams({ input: JSON.stringify(slice), equate_il: this.options.equate, @@ -77,9 +83,24 @@ export abstract class UnipeptSubcommand { }); } - // TODO: implement - private constructSelectedFields(): RegExp[] { - return []; + private getSelectedFields(): RegExp[] { + if (this.selectedFields) return this.getSelectedFields(); + + const fields = (this.options.fields as string[]).flatMap(f => f.split(",")); + if (this.fasta && fields.length > 0) { + fields.push(...this.requiredFields()); + } + this.selectedFields = fields.map(f => this.globToRegex(f)); + + return this.selectedFields; + } + + private get batchSize(): number { + if (this.options.batch) { + return +this.options.batch; + } else { + return this.defaultBatchSize(); + } } /** @@ -108,4 +129,8 @@ export abstract class UnipeptSubcommand { return `http://${host}`; } } + + private globToRegex(glob: string): RegExp { + return new RegExp(glob.replace(/\*/g, ".*")); + } } From bcd9535dea452a0b67b8bc7e3721220805dbfefb Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Mon, 8 Jul 2024 09:54:54 +0200 Subject: [PATCH 08/16] add basic csv formatter --- lib/commands/unipept/unipept_subcommand.ts | 20 +++++++++++++++++--- lib/formatters/csv_formatter.ts | 21 +++++++++++++++++++++ lib/formatters/formatter.ts | 19 +++++++++++++++++++ lib/formatters/formatter_factory.ts | 11 +++++++++++ package.json | 3 ++- yarn.lock | 5 +++++ 6 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 lib/formatters/csv_formatter.ts create mode 100644 lib/formatters/formatter.ts create mode 100644 lib/formatters/formatter_factory.ts diff --git a/lib/commands/unipept/unipept_subcommand.ts b/lib/commands/unipept/unipept_subcommand.ts index dca448ce..fc43d6b6 100644 --- a/lib/commands/unipept/unipept_subcommand.ts +++ b/lib/commands/unipept/unipept_subcommand.ts @@ -2,6 +2,8 @@ import { Command, Option } from "commander"; import { createReadStream, readFileSync } from "fs"; import { createInterface } from "node:readline"; import { Interface } from "readline"; +import { Formatter } from "../../formatters/formatter.js"; +import { FormatterFactory } from "../../formatters/formatter_factory.js"; export abstract class UnipeptSubcommand { public command: Command; @@ -14,6 +16,8 @@ export abstract class UnipeptSubcommand { url?: string; selectedFields?: RegExp[]; fasta: boolean; + formatter?: Formatter; + firstBatch = true; constructor(name: string) { this.name = name; @@ -48,6 +52,7 @@ export abstract class UnipeptSubcommand { this.options = options; this.host = this.getHost(); this.url = `${this.host}/api/v2/${this.name}.json`; + this.formatter = FormatterFactory.getFormatter(this.options.format); let slice = []; @@ -62,6 +67,8 @@ export abstract class UnipeptSubcommand { } async processBatch(slice: string[]): Promise { + if (!this.formatter) throw new Error("Formatter not set"); + const r = await fetch(this.url as string, { method: "POST", body: this.constructRequestBody(slice), @@ -70,7 +77,14 @@ export abstract class UnipeptSubcommand { "User-Agent": this.user_agent, } }); - console.log(await r.json()); + const result = await r.json(); + + if (this.firstBatch) { + this.firstBatch = false; + process.stdout.write(this.formatter.header(result, this.fasta)); + } + + process.stdout.write(this.formatter.format(result, this.fasta)); } private constructRequestBody(slice: string[]): URLSearchParams { @@ -84,9 +98,9 @@ export abstract class UnipeptSubcommand { } private getSelectedFields(): RegExp[] { - if (this.selectedFields) return this.getSelectedFields(); + if (this.selectedFields) return this.selectedFields; - const fields = (this.options.fields as string[]).flatMap(f => f.split(",")); + const fields = (this.options.select as string[])?.flatMap(f => f.split(",")) ?? []; if (this.fasta && fields.length > 0) { fields.push(...this.requiredFields()); } diff --git a/lib/formatters/csv_formatter.ts b/lib/formatters/csv_formatter.ts new file mode 100644 index 00000000..39b8657f --- /dev/null +++ b/lib/formatters/csv_formatter.ts @@ -0,0 +1,21 @@ +import { Formatter } from "./formatter.js"; +import { stringify } from "csv-stringify/sync"; + +export class CSVFormatter extends Formatter { + + header(sampleData: { [key: string]: string }[], fastaMapper?: boolean | undefined): string { + return stringify([this.getKeys(sampleData, fastaMapper)]); + } + + footer(): string { + return ""; + } + + convert(data: object[]): string { + return stringify(data); + } + + getKeys(data: { [key: string]: string }[], fastaMapper?: boolean | undefined): string[] { + return fastaMapper ? ["fasta_header", ...Object.keys(data[0])] : Object.keys(data[0]); + } +} diff --git a/lib/formatters/formatter.ts b/lib/formatters/formatter.ts new file mode 100644 index 00000000..4f1ffcca --- /dev/null +++ b/lib/formatters/formatter.ts @@ -0,0 +1,19 @@ +import { CSVFormatter } from "./csv_formatter.js"; + +export abstract class Formatter { + + abstract header(sampleData: object, fastaMapper?: boolean): string; + abstract footer(): string; + abstract convert(data: any, first?: boolean): string; + + format(data, fastaMapper?: boolean, first?: boolean): string { + if (fastaMapper) { + data = this.integrateFastaHeaders(data, fastaMapper); + } + return this.convert(data, first); + } + + integrateFastaHeaders(data: any, fastaMapper: boolean): any { + return data; + } +} diff --git a/lib/formatters/formatter_factory.ts b/lib/formatters/formatter_factory.ts new file mode 100644 index 00000000..b350df77 --- /dev/null +++ b/lib/formatters/formatter_factory.ts @@ -0,0 +1,11 @@ +import { CSVFormatter } from "./csv_formatter.js"; +import { Formatter } from "./formatter.js"; + +export class FormatterFactory { + static getFormatter(name: string): Formatter { + if (name === "csv") { + return new CSVFormatter(); + } + return new CSVFormatter(); + } +} diff --git a/package.json b/package.json index 3e69dd9e..fc1ff4c4 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "uniprot": "yarn run tsx bin/uniprot.ts" }, "dependencies": { - "commander": "^12.1.0" + "commander": "^12.1.0", + "csv-stringify": "^6.5.0" }, "devDependencies": { "@eslint/js": "^9.5.0", diff --git a/yarn.lock b/yarn.lock index bb0cd995..90d703b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1307,6 +1307,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +csv-stringify@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-6.5.0.tgz#7b1491893c917e018a97de9bf9604e23b88647c2" + integrity sha512-edlXFVKcUx7r8Vx5zQucsuMg4wb/xT6qyz+Sr1vnLrdXqlLD1+UKyWNyZ9zn6mUW1ewmGxrpVwAcChGF0HQ/2Q== + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.5" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" From 0ad8004e1fce2795fbe38f76cc61687200646ff9 Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Mon, 8 Jul 2024 10:02:22 +0200 Subject: [PATCH 09/16] write to file if needed --- lib/commands/unipept/unipept_subcommand.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/commands/unipept/unipept_subcommand.ts b/lib/commands/unipept/unipept_subcommand.ts index fc43d6b6..78777153 100644 --- a/lib/commands/unipept/unipept_subcommand.ts +++ b/lib/commands/unipept/unipept_subcommand.ts @@ -1,5 +1,5 @@ import { Command, Option } from "commander"; -import { createReadStream, readFileSync } from "fs"; +import { createReadStream, createWriteStream, readFileSync } from "fs"; import { createInterface } from "node:readline"; import { Interface } from "readline"; import { Formatter } from "../../formatters/formatter.js"; @@ -14,10 +14,11 @@ export abstract class UnipeptSubcommand { user_agent: string; host = "https://api.unipept.ugent.be"; url?: string; - selectedFields?: RegExp[]; - fasta: boolean; formatter?: Formatter; + outputStream: NodeJS.WritableStream = process.stdout; firstBatch = true; + selectedFields?: RegExp[]; + fasta: boolean; constructor(name: string) { this.name = name; @@ -53,6 +54,9 @@ export abstract class UnipeptSubcommand { this.host = this.getHost(); this.url = `${this.host}/api/v2/${this.name}.json`; this.formatter = FormatterFactory.getFormatter(this.options.format); + if (this.options.output) { + this.outputStream = createWriteStream(this.options.output); + } let slice = []; @@ -81,10 +85,10 @@ export abstract class UnipeptSubcommand { if (this.firstBatch) { this.firstBatch = false; - process.stdout.write(this.formatter.header(result, this.fasta)); + this.outputStream.write(this.formatter.header(result, this.fasta)); } - process.stdout.write(this.formatter.format(result, this.fasta)); + this.outputStream.write(this.formatter.format(result, this.fasta)); } private constructRequestBody(slice: string[]): URLSearchParams { From 8f9334a2aca04f277e6222cbf321f6475d8f9f7c Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Mon, 8 Jul 2024 10:07:22 +0200 Subject: [PATCH 10/16] support no-header --- lib/commands/unipept/unipept_subcommand.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/unipept/unipept_subcommand.ts b/lib/commands/unipept/unipept_subcommand.ts index 78777153..3ebecee6 100644 --- a/lib/commands/unipept/unipept_subcommand.ts +++ b/lib/commands/unipept/unipept_subcommand.ts @@ -83,7 +83,7 @@ export abstract class UnipeptSubcommand { }); const result = await r.json(); - if (this.firstBatch) { + if (this.firstBatch && this.options.header) { this.firstBatch = false; this.outputStream.write(this.formatter.header(result, this.fasta)); } From beb414c44b5f5d99528522d1b7660dd55c5d4b84 Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Mon, 8 Jul 2024 11:00:57 +0200 Subject: [PATCH 11/16] fix lint --- lib/formatters/formatter.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/formatters/formatter.ts b/lib/formatters/formatter.ts index 4f1ffcca..39d4202d 100644 --- a/lib/formatters/formatter.ts +++ b/lib/formatters/formatter.ts @@ -1,19 +1,18 @@ -import { CSVFormatter } from "./csv_formatter.js"; - export abstract class Formatter { abstract header(sampleData: object, fastaMapper?: boolean): string; abstract footer(): string; - abstract convert(data: any, first?: boolean): string; + abstract convert(data: { [key: string]: string }[], first?: boolean): string; - format(data, fastaMapper?: boolean, first?: boolean): string { + format(data: { [key: string]: string }[], fastaMapper?: boolean, first?: boolean): string { if (fastaMapper) { data = this.integrateFastaHeaders(data, fastaMapper); } return this.convert(data, first); } - integrateFastaHeaders(data: any, fastaMapper: boolean): any { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + integrateFastaHeaders(data: { [key: string]: string }[], fastaMapper: boolean): { [key: string]: string }[] { return data; } } From 5c9e17f4d942bb6a41f70d5813c62f895e4bb8de Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Tue, 30 Jul 2024 10:48:56 +0200 Subject: [PATCH 12/16] add test for unipept --- tests/commands/unipept.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/commands/unipept.test.ts b/tests/commands/unipept.test.ts index e69de29b..c2b2e340 100644 --- a/tests/commands/unipept.test.ts +++ b/tests/commands/unipept.test.ts @@ -0,0 +1,7 @@ +import { Unipept } from '../../lib/commands/unipept'; + +test('test single argument', async () => { + const command = new Unipept(); + const commandNames = command.program.commands.map(c => c.name()); + expect(commandNames).toContain("pept2lca"); +}); From e0b691a7fb0817e7daa57fbbbc8e5b7b7843c21d Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Tue, 30 Jul 2024 14:57:33 +0200 Subject: [PATCH 13/16] add formatter tests --- lib/formatters/formatter.ts | 6 ++--- tests/commands/unipept.test.ts | 2 +- tests/formatters/csv_formatter.test.ts | 30 ++++++++++++++++++++++ tests/formatters/formatter_factory.test.ts | 11 ++++++++ tests/formatters/test_object.ts | 21 +++++++++++++++ 5 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 tests/formatters/csv_formatter.test.ts create mode 100644 tests/formatters/formatter_factory.test.ts create mode 100644 tests/formatters/test_object.ts diff --git a/lib/formatters/formatter.ts b/lib/formatters/formatter.ts index 39d4202d..1c8ecf02 100644 --- a/lib/formatters/formatter.ts +++ b/lib/formatters/formatter.ts @@ -2,9 +2,9 @@ export abstract class Formatter { abstract header(sampleData: object, fastaMapper?: boolean): string; abstract footer(): string; - abstract convert(data: { [key: string]: string }[], first?: boolean): string; + abstract convert(data: object[], first?: boolean): string; - format(data: { [key: string]: string }[], fastaMapper?: boolean, first?: boolean): string { + format(data: object[], fastaMapper?: boolean, first?: boolean): string { if (fastaMapper) { data = this.integrateFastaHeaders(data, fastaMapper); } @@ -12,7 +12,7 @@ export abstract class Formatter { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - integrateFastaHeaders(data: { [key: string]: string }[], fastaMapper: boolean): { [key: string]: string }[] { + integrateFastaHeaders(data: object[], fastaMapper: boolean): object[] { return data; } } diff --git a/tests/commands/unipept.test.ts b/tests/commands/unipept.test.ts index c2b2e340..cdda14dd 100644 --- a/tests/commands/unipept.test.ts +++ b/tests/commands/unipept.test.ts @@ -1,6 +1,6 @@ import { Unipept } from '../../lib/commands/unipept'; -test('test single argument', async () => { +test('test if all commands are available', async () => { const command = new Unipept(); const commandNames = command.program.commands.map(c => c.name()); expect(commandNames).toContain("pept2lca"); diff --git a/tests/formatters/csv_formatter.test.ts b/tests/formatters/csv_formatter.test.ts new file mode 100644 index 00000000..847f086b --- /dev/null +++ b/tests/formatters/csv_formatter.test.ts @@ -0,0 +1,30 @@ +import { FormatterFactory } from "../../lib/formatters/formatter_factory"; +import { TestObject } from "./test_object"; + +const formatter = FormatterFactory.getFormatter("csv"); + +test('test header', () => { + const fasta = [["peptide", ">test"]]; + const object = [TestObject.testObject(), TestObject.testObject()]; + expect(formatter.header(object)).toBe(TestObject.asCsvHeader()); + //expect(formatter.header(object, fasta)).toBe(`fasta_header,${TestObject.asCsvHeader()}`); +}); + +test('test footer', () => { + expect(formatter.footer()).toBe(""); +}); + +test('test convert', () => { + const object = [TestObject.testObject(), TestObject.testObject()]; + const csv = [TestObject.asCsv(), TestObject.asCsv(), ""].join("\n"); + + expect(formatter.convert(object, true)).toBe(csv); + expect(formatter.convert(object, false)).toBe(csv); +}); + +test('test format with fasta', () => { + const fasta = [['>test', '5']]; + const object = [TestObject.testObject(), TestObject.testObject()]; + const csv = [`>test,${TestObject.asCsv()}`, TestObject.asCsv(), ""].join("\n"); + //expect(formatter.format(object, fasta, false)).toBe(csv); +}); diff --git a/tests/formatters/formatter_factory.test.ts b/tests/formatters/formatter_factory.test.ts new file mode 100644 index 00000000..05933522 --- /dev/null +++ b/tests/formatters/formatter_factory.test.ts @@ -0,0 +1,11 @@ +import { FormatterFactory } from "../../lib/formatters/formatter_factory"; + +test('test if default formatter is csv', async () => { + const formatter = FormatterFactory.getFormatter("foo"); + expect(formatter.constructor.name).toBe("CSVFormatter"); +}); + +test('test if csv formatter is csv', async () => { + const formatter = FormatterFactory.getFormatter("csv"); + expect(formatter.constructor.name).toBe("CSVFormatter"); +}); diff --git a/tests/formatters/test_object.ts b/tests/formatters/test_object.ts new file mode 100644 index 00000000..097ffc57 --- /dev/null +++ b/tests/formatters/test_object.ts @@ -0,0 +1,21 @@ +export class TestObject { + static testObject() { + return { "integer": 5, "string": "string", "list": ["a", 2, false] }; + } + + static asJson() { + return '{"integer":5,"string":"string","list":["a",2,false]}'; + } + + static asXml() { + return '5stringa2false'; + } + + static asCsv() { + return '5,string,"[""a"",2,false]"'; + } + + static asCsvHeader() { + return "integer,string,list\n"; + } +} From e1589da2519a9bd9b2b7c45408d6bce7c0022d70 Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Tue, 30 Jul 2024 15:02:32 +0200 Subject: [PATCH 14/16] linter --- tests/formatters/csv_formatter.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/formatters/csv_formatter.test.ts b/tests/formatters/csv_formatter.test.ts index 847f086b..2b9a3b85 100644 --- a/tests/formatters/csv_formatter.test.ts +++ b/tests/formatters/csv_formatter.test.ts @@ -4,7 +4,7 @@ import { TestObject } from "./test_object"; const formatter = FormatterFactory.getFormatter("csv"); test('test header', () => { - const fasta = [["peptide", ">test"]]; + //const fasta = [["peptide", ">test"]]; const object = [TestObject.testObject(), TestObject.testObject()]; expect(formatter.header(object)).toBe(TestObject.asCsvHeader()); //expect(formatter.header(object, fasta)).toBe(`fasta_header,${TestObject.asCsvHeader()}`); @@ -23,8 +23,8 @@ test('test convert', () => { }); test('test format with fasta', () => { - const fasta = [['>test', '5']]; - const object = [TestObject.testObject(), TestObject.testObject()]; - const csv = [`>test,${TestObject.asCsv()}`, TestObject.asCsv(), ""].join("\n"); + //const fasta = [['>test', '5']]; + //const object = [TestObject.testObject(), TestObject.testObject()]; + //const csv = [`>test,${TestObject.asCsv()}`, TestObject.asCsv(), ""].join("\n"); //expect(formatter.format(object, fasta, false)).toBe(csv); }); From 819f1a61d5e23953146b120cd49c7881cdbdac5d Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Tue, 30 Jul 2024 15:14:28 +0200 Subject: [PATCH 15/16] refactor passing arguments programatically --- lib/commands/base_command.ts | 12 +++++------- lib/commands/peptfilter.ts | 6 +++--- lib/commands/prot2pept.ts | 6 +++--- lib/commands/unipept.ts | 6 +++--- lib/commands/uniprot.ts | 6 +++--- tests/commands/peptfilter.test.ts | 4 ++-- tests/commands/prot2pept.test.ts | 4 ++-- tests/commands/uniprot.test.ts | 20 ++++++++++---------- 8 files changed, 31 insertions(+), 33 deletions(-) diff --git a/lib/commands/base_command.ts b/lib/commands/base_command.ts index 35c7b795..efa87e6a 100644 --- a/lib/commands/base_command.ts +++ b/lib/commands/base_command.ts @@ -10,16 +10,14 @@ import { readFileSync } from "fs"; */ export abstract class BaseCommand { public program: Command; - args: string[] | undefined; version: string; - constructor(options?: { exitOverride?: boolean, suppressOutput?: boolean, args?: string[] }) { + constructor(options?: { exitOverride?: boolean, suppressOutput?: boolean }) { this.version = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version; this.program = this.create(options); - this.args = options?.args; } - abstract run(): void; + abstract run(args?: string[]): void; /** * Create sets up the command line program. Implementing classes can add additional options. @@ -47,10 +45,10 @@ export abstract class BaseCommand { /** * This allows us to pass a custom list of strings as arguments to the command during testing. */ - parseArguments() { - if (this.args) { + parseArguments(args?: string[]) { + if (args) { // custom arg parsing to be able to inject args for testing - this.program.parse(this.args, { from: "user" }); + this.program.parse(args, { from: "user" }); } else { this.program.parse(); } diff --git a/lib/commands/peptfilter.ts b/lib/commands/peptfilter.ts index f8435dc4..0bdb17ec 100644 --- a/lib/commands/peptfilter.ts +++ b/lib/commands/peptfilter.ts @@ -7,7 +7,7 @@ export class Peptfilter extends BaseCommand { The input should have one peptide per line. FASTA headers are preserved in the output, so that peptides remain bundled.`; - constructor(options?: { exitOverride?: boolean, suppressOutput?: boolean, args?: string[] }) { + constructor(options?: { exitOverride?: boolean, suppressOutput?: boolean }) { super(options); this.program @@ -24,8 +24,8 @@ The input should have one peptide per line. FASTA headers are preserved in the o * async iterators. This alternative implementation runs in 2.5 seconds. However, I decided that the async iterator implementation is * both more readable and more in line with the implementation of the other commands. */ - async run() { - this.parseArguments(); + async run(args?: string[]) { + this.parseArguments(args); const minLen = this.program.opts().minlen; const maxlen = this.program.opts().maxlen; const lacks = this.program.opts().lacks || []; diff --git a/lib/commands/prot2pept.ts b/lib/commands/prot2pept.ts index a64d862a..4c33ed7c 100644 --- a/lib/commands/prot2pept.ts +++ b/lib/commands/prot2pept.ts @@ -8,7 +8,7 @@ export class Prot2pept extends BaseCommand { The input should have either one protein sequence per line or contain a FASTA formatted list of protein sequences. FASTA headers are preserved in the output, so that peptides can be bundled per protein sequence. `; - constructor(options?: { exitOverride?: boolean, suppressOutput?: boolean, args?: string[] }) { + constructor(options?: { exitOverride?: boolean, suppressOutput?: boolean }) { super(options); this.program @@ -21,8 +21,8 @@ The input should have either one protein sequence per line or contain a FASTA fo * Performance note: Just as with peptfilter, this implementation can be made faster by using line events instead of * async iterators. */ - async run() { - this.parseArguments(); + async run(args?: string[]) { + this.parseArguments(args); let pattern; try { diff --git a/lib/commands/unipept.ts b/lib/commands/unipept.ts index 553a5fc2..24c1b29a 100644 --- a/lib/commands/unipept.ts +++ b/lib/commands/unipept.ts @@ -13,7 +13,7 @@ Subcommands that start with pept expect a list of tryptic peptides as input. Sub The command will give priority to the first way the input is passed, in the order as listed above. Text files and standard input should have one tryptic peptide or one NCBI Taxonomy Identifier per line.`; - constructor(options?: { exitOverride?: boolean, suppressOutput?: boolean, args?: string[] }) { + constructor(options?: { exitOverride?: boolean, suppressOutput?: boolean }) { super(options); this.program @@ -22,7 +22,7 @@ The command will give priority to the first way the input is passed, in the orde .addCommand(new Pept2lca().command); } - async run() { - this.parseArguments(); + async run(args?: string[]) { + this.parseArguments(args); } } diff --git a/lib/commands/uniprot.ts b/lib/commands/uniprot.ts index 11f5e57b..e0946251 100644 --- a/lib/commands/uniprot.ts +++ b/lib/commands/uniprot.ts @@ -16,7 +16,7 @@ The command will give priority to the first way UniProt Accession Numbers are pa The uniprot command yields just the protein sequences as a default, but can return several formats.`; - constructor(options?: { exitOverride?: boolean, suppressOutput?: boolean, args?: string[] }) { + constructor(options?: { exitOverride?: boolean, suppressOutput?: boolean }) { super(options); this.program @@ -26,8 +26,8 @@ The uniprot command yields just the protein sequences as a default, but can retu .addOption(new Option("-f, --format ", `output format`).choices(Uniprot.VALID_FORMATS).default("sequence")); } - async run() { - this.parseArguments(); + async run(args?: string[]) { + this.parseArguments(args); const format = this.program.opts().format; const accessions = this.program.args; diff --git a/tests/commands/peptfilter.test.ts b/tests/commands/peptfilter.test.ts index 0318fde2..0f13096b 100644 --- a/tests/commands/peptfilter.test.ts +++ b/tests/commands/peptfilter.test.ts @@ -80,8 +80,8 @@ test('test if it passes fasta from stdin', async () => { test('test complex example from stdin', async () => { const stdin = mock.stdin(); - const command = new Peptfilter({ args: ["--minlen", "4", "--maxlen", "10", "--lacks", "B", "--contains", "A"] }); - const run = command.run(); + const command = new Peptfilter(); + const run = command.run(["--minlen", "4", "--maxlen", "10", "--lacks", "B", "--contains", "A"]); stdin.send("A\n"); stdin.send("AAAAAAAAAAA\n"); diff --git a/tests/commands/prot2pept.test.ts b/tests/commands/prot2pept.test.ts index ca01533a..80d7e504 100644 --- a/tests/commands/prot2pept.test.ts +++ b/tests/commands/prot2pept.test.ts @@ -111,8 +111,8 @@ test('test fasta input 3', async () => { test('test custom pattern', async () => { const stdin = mock.stdin(); - const command = new Prot2pept({ args: ["--pattern", "([KR])([^A])"] }); - const run = command.run(); + const command = new Prot2pept(); + const run = command.run(["--pattern", "([KR])([^A])"]); stdin.send("AALTERAALTERPAALTER\n"); stdin.end(); diff --git a/tests/commands/uniprot.test.ts b/tests/commands/uniprot.test.ts index 59ba4f7f..27b7f304 100644 --- a/tests/commands/uniprot.test.ts +++ b/tests/commands/uniprot.test.ts @@ -17,8 +17,8 @@ beforeEach(() => { }); test('test single argument', async () => { - const command = new Uniprot({ args: ["Q6GZX3"] }); - await command.run(); + const command = new Uniprot(); + await command.run(["Q6GZX3"]); expect(writeSpy).toHaveBeenCalledTimes(1); expect(errorSpy).toHaveBeenCalledTimes(0); @@ -26,8 +26,8 @@ test('test single argument', async () => { }); test('test two arguments', async () => { - const command = new Uniprot({ args: ["Q6GZX3", "Q6GZX4"] }); - await command.run(); + const command = new Uniprot(); + await command.run(["Q6GZX3", "Q6GZX4"]); expect(writeSpy).toHaveBeenCalledTimes(2); expect(errorSpy).toHaveBeenCalledTimes(0); @@ -35,8 +35,8 @@ test('test two arguments', async () => { }); test('test fasta output', async () => { - const command = new Uniprot({ args: ["--format", "fasta", "Q6GZX3", "Q6GZX4"] }); - await command.run(); + const command = new Uniprot(); + await command.run(["--format", "fasta", "Q6GZX3", "Q6GZX4"]); expect(writeSpy).toHaveBeenCalledTimes(2); expect(errorSpy).toHaveBeenCalledTimes(0); @@ -78,16 +78,16 @@ test('test double line stdin', async () => { }); test('test on invalid id', async () => { - const command = new Uniprot({ args: ["Bart"] }); - await command.run(); + const command = new Uniprot(); + await command.run(["Bart"]); expect(errorSpy).toHaveBeenCalledTimes(1); }); test('test all valid formats', async () => { for (const format of Uniprot.VALID_FORMATS) { - const command = new Uniprot({ args: ["--format", format, "Q6GZX3"] }); - await command.run(); + const command = new Uniprot(); + await command.run(["--format", format, "Q6GZX3"]); expect(errorSpy).toHaveBeenCalledTimes(0); } From f7dcf0e5b2c0a8c51f1940b38f6acaad3f41bd00 Mon Sep 17 00:00:00 2001 From: Bart Mesuere Date: Wed, 31 Jul 2024 15:01:59 +0200 Subject: [PATCH 16/16] add basic command runner tests --- lib/commands/unipept/unipept_subcommand.ts | 2 +- .../unipept/unipept_subcommand.test.ts | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/commands/unipept/unipept_subcommand.test.ts diff --git a/lib/commands/unipept/unipept_subcommand.ts b/lib/commands/unipept/unipept_subcommand.ts index 3ebecee6..a9e6e593 100644 --- a/lib/commands/unipept/unipept_subcommand.ts +++ b/lib/commands/unipept/unipept_subcommand.ts @@ -149,6 +149,6 @@ export abstract class UnipeptSubcommand { } private globToRegex(glob: string): RegExp { - return new RegExp(glob.replace(/\*/g, ".*")); + return new RegExp(`^${glob.replace(/\*/g, ".*")}$`); } } diff --git a/tests/commands/unipept/unipept_subcommand.test.ts b/tests/commands/unipept/unipept_subcommand.test.ts new file mode 100644 index 00000000..de816a36 --- /dev/null +++ b/tests/commands/unipept/unipept_subcommand.test.ts @@ -0,0 +1,58 @@ +import { Interface } from 'readline'; +import { Pept2lca } from '../../../lib/commands/unipept/pept2lca'; + +test('test command setup', () => { + const command = new Pept2lca(); + expect(command.name).toBe("pept2lca"); + expect(command.user_agent).toMatch(/^unipept-cli/); + expect(command.command.name()).toBe("pept2lca"); +}); + +test('test correct host', () => { + const command = new Pept2lca(); + + expect(command.host).toBe("https://api.unipept.ugent.be"); + expect(command["getHost"]()).toBe("https://api.unipept.ugent.be"); + + command.options.host = "https://optionshost"; + expect(command["getHost"]()).toBe("https://optionshost"); + + command.options.host = "http://optionshost"; + expect(command["getHost"]()).toBe("http://optionshost"); + + command.options.host = "optionshost"; + expect(command["getHost"]()).toBe("http://optionshost"); +}); + +test('test correct inputIterator', async () => { + const command = new Pept2lca(); + + // should be stdin + let input = command["getInputIterator"]([]) as Interface; + expect(input).toBeInstanceOf(Interface); + input.close(); + + // should be a (non-existant) file and error + input = command["getInputIterator"]([], "filename") as Interface; + input.on("error", (e) => { + expect(e.toString()).toMatch(/no such file/); + }); + + // should be array + const inputArray = command["getInputIterator"](["A", "B"]); + expect(inputArray).toBeInstanceOf(Array); +}); + +test('test selected fields parsing', () => { + const command = new Pept2lca(); + + command.options.select = ["a,b,c"]; + expect(command["getSelectedFields"]()).toStrictEqual([/^a$/, /^b$/, /^c$/]); +}); + +test('test selected fields with wildcards', () => { + const command = new Pept2lca(); + + command.options.select = ["taxon*,name"]; + expect(command["getSelectedFields"]()).toStrictEqual([/^taxon.*$/, /^name$/]); +});