Made base classes for zip-based metadata readers

This commit is contained in:
Kir_Antipov 2023-03-08 11:47:32 +00:00
parent 917a5130f1
commit a8cce57c4b
2 changed files with 215 additions and 0 deletions

View file

@ -0,0 +1,115 @@
import { Awaitable } from "@/utils/types";
import { StreamZipAsync, async as ZipArchive } from "node-stream-zip";
import { PathLike } from "node:fs";
import { LoaderMetadata } from "./loader-metadata";
import { LoaderMetadataReader } from "./loader-metadata-reader";
/**
* Provides a base for reading metadata from zipped files for various loaders.
*
* @template TMetadata - Represents the processed metadata object.
* @template TRawMetadata - Represents the raw metadata object to be transformed.
*/
export abstract class ZippedLoaderMetadataReader<TMetadata extends LoaderMetadata, TRawMetadata> implements LoaderMetadataReader<TMetadata> {
/**
* The name of the entry inside the zipped file to read.
*/
private readonly _entry: string;
/**
* Constructs a new {@link ZippedLoaderMetadataReader} instance.
*
* @param entry - The name of the entry inside the zipped file to read.
*/
protected constructor(entry: string) {
this._entry = entry;
}
/**
* Reads the metadata file from a zipped file at the given path.
*
* @param path - The path to the zipped file.
*
* @returns The metadata object, or `undefined` if the zipped file cannot be read.
*/
async readMetadataFile(path: PathLike): Promise<TMetadata | undefined> {
let zip = undefined as StreamZipAsync;
try {
zip = new ZipArchive({ file: path as string });
const buffer = await zip.entryData(this._entry);
if (!buffer) {
return undefined;
}
const rawMetadata = await this.readRawMetadata(buffer);
return await this.createMetadata(rawMetadata);
} catch {
return undefined;
} finally {
await zip?.close().catch(() => undefined);
}
}
/**
* Reads the raw metadata from a buffer.
*
* @param buffer - The buffer containing the raw metadata.
*
* @returns The raw metadata object.
*/
protected abstract readRawMetadata(buffer: Buffer): Promise<TRawMetadata>;
/**
* Creates a metadata object from the raw metadata.
*
* @param config - The raw metadata object.
*
* @returns The metadata object.
*/
protected abstract createMetadata(config: TRawMetadata): Promise<TMetadata>;
}
/**
* Provides a base for reading metadata from text-based files within zipped files.
*
* @template TMetadata - Represents the processed metadata object.
* @template TRawMetadata - Represents the raw metadata object to be transformed.
*/
export abstract class ZippedTextLoaderMetadataReader<TMetadata extends LoaderMetadata, TRawMetadata> extends ZippedLoaderMetadataReader<TMetadata, TRawMetadata> {
/**
* A function to transform the raw metadata into a processed metadata object.
*/
private readonly _factory: (raw: TRawMetadata) => Awaitable<TMetadata>;
/**
* A function to parse the text content into a raw metadata object.
*/
private readonly _parser: (text: string) => Awaitable<TRawMetadata>;
/**
* Constructs a new {@link ZippedTextLoaderMetadataReader} instance.
*
* @param entry - The name of the entry inside the zipped file to read.
* @param factory - A function to transform the raw metadata into a processed metadata object.
* @param parser - A function to parse the text content into a raw metadata object.
*/
protected constructor(entry: string, factory: (raw: TRawMetadata) => Awaitable<TMetadata>, parser: (text: string) => Awaitable<TRawMetadata>) {
super(entry);
this._factory = factory;
this._parser = parser;
}
/**
* @inheritdoc
*/
protected async readRawMetadata(buffer: Buffer): Promise<TRawMetadata> {
return await this._parser(buffer.toString());
}
/**
* @inheritdoc
*/
protected async createMetadata(config: TRawMetadata): Promise<TMetadata> {
return await this._factory(config);
}
}

View file

@ -0,0 +1,100 @@
import { zipContent } from "@/../tests/utils/zip-utils";
import { LoaderMetadata } from "@/loaders/loader-metadata";
import mockFs from "mock-fs";
import { ZippedLoaderMetadataReader, ZippedTextLoaderMetadataReader } from "@/loaders/zipped-loader-metadata-reader";
class MockZippedLoaderMetadataReader extends ZippedLoaderMetadataReader<LoaderMetadata, string> {
constructor(entry: string) {
super(entry);
}
protected readRawMetadata(buffer: Buffer): Promise<string> {
return Promise.resolve(buffer.toString());
}
protected createMetadata(config: string): Promise<LoaderMetadata> {
return Promise.resolve({ id: config } as LoaderMetadata);
}
}
class MockZippedTextLoaderMetadataReader extends ZippedTextLoaderMetadataReader<LoaderMetadata, string> {
constructor(entry: string, factory: (raw: string) => LoaderMetadata, parser: (text: string) => string) {
super(entry, factory, parser);
}
}
beforeEach(async () => {
mockFs({
"test.zip": await zipContent("Test", "test.txt"),
});
});
afterEach(() => {
mockFs.restore();
});
describe("ZippedLoaderMetadataReader", () => {
test("reads metadata file from a zipped file at the given path", async () => {
const reader = new MockZippedLoaderMetadataReader("test.txt");
const metadata = await reader.readMetadataFile("test.zip");
expect(metadata).toMatchObject({ id: "Test" });
});
test("returns undefined if the given path does not exist", async () => {
const reader = new MockZippedLoaderMetadataReader("test.txt");
const metadata = await reader.readMetadataFile("");
expect(metadata).toBeUndefined();
});
test("returns undefined if the zip entry does not exist", async () => {
const reader = new MockZippedLoaderMetadataReader("test");
const metadata = await reader.readMetadataFile("test.zip");
expect(metadata).toBeUndefined();
});
});
describe("ZippedTextLoaderMetadataReader", () => {
test("reads metadata file from a zipped file at the given path", async () => {
const factory = jest.fn().mockImplementation(x => ({ id: x }));
const parse = jest.fn().mockImplementation(x => [...String(x)].reverse().join(""));
const reader = new MockZippedTextLoaderMetadataReader("test.txt", factory, parse);
const metadata = await reader.readMetadataFile("test.zip");
expect(metadata).toMatchObject({ id: "tseT" });
expect(factory).toHaveBeenCalledTimes(1);
expect(factory).toHaveBeenCalledWith("tseT");
expect(parse).toHaveBeenCalledTimes(1);
expect(parse).toHaveBeenCalledWith("Test");
});
test("returns undefined if the given path does not exist", async () => {
const factory = jest.fn();
const parse = jest.fn();
const reader = new MockZippedTextLoaderMetadataReader("test.txt", factory, parse);
const metadata = await reader.readMetadataFile("");
expect(metadata).toBeUndefined();
expect(factory).not.toHaveBeenCalled();
expect(parse).not.toHaveBeenCalled();
});
test("returns undefined if the zip entry does not exist", async () => {
const factory = jest.fn();
const parse = jest.fn();
const reader = new MockZippedTextLoaderMetadataReader("test", factory, parse);
const metadata = await reader.readMetadataFile("test.zip");
expect(metadata).toBeUndefined();
expect(factory).not.toHaveBeenCalled();
expect(parse).not.toHaveBeenCalled();
});
});